diff --git a/.dockerignore b/.dockerignore index 36bffe9..5c522c6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,4 +3,4 @@ /services.md /APIs.md /README.md -/benchmarking.cpp +/benchmarking.cpp \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..af3b74d --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,41 @@ +name: Build +on: + push: + branches: + - main +jobs: + release: + name: Push Docker image to GitHub Packages + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Checkout the repo + uses: actions/checkout@v2 + - name: Login to GitHub Docker Registry + uses: docker/login-action@v1 + with: + registry: docker.pkg.github.com + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Format repository + run: | + echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV + - name: Build container image + uses: docker/build-push-action@v2 + with: + push: true + tags: | + docker.pkg.github.com/${{ env.IMAGE_REPOSITORY }}/ccash:${{ github.sha }} + docker.pkg.github.com/${{ env.IMAGE_REPOSITORY }}/ccash:latest + trigger-deploy: + needs: release + runs-on: ubuntu-latest + steps: + - run: | + curl -X POST \ + -H 'Accept: application/vnd.github.v3+json' \ + -H 'Authorization: Bearer ${{ secrets.CCASH_DEPLOY_TOKEN }}' \ + https://api.github.com/repos/${{ github.repository }}/actions/workflows/deploy.yaml/dispatches \ + -d '{"ref":"main"}' diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 997bdbc..c93b4be 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,31 +1,59 @@ -name: Publish Staging -on: - push: - branches: - - main +name: Deploy +on: workflow_dispatch jobs: release: - name: Push Docker image to GitHub Packages + name: Deploy Docker image to remote machine runs-on: ubuntu-latest - permissions: - packages: write - contents: read steps: - - name: Checkout the repo - uses: actions/checkout@v2 - - name: Login to GitHub Docker Registry - uses: docker/login-action@v1 + - name: Write CCASH_CONFIG_JSON to remote filesystem + uses: appleboy/ssh-action@master with: - registry: docker.pkg.github.com - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + host: ${{ secrets.CCASH_DOMAIN }} + username: root + key: ${{ secrets.CCASH_SSH_KEY }} + script: "echo '${{ secrets.CCASH_CONFIG_JSON }}' > $(pwd)/config.json" + - name: Write CCASH_USERS_JSON to remote filesystem if it doesn't already exist + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.CCASH_DOMAIN }} + username: root + key: ${{ secrets.CCASH_SSH_KEY }} + script: "[[ -f $(pwd)/users.json ]] && echo 'users.json already exists' || echo '${{ secrets.CCASH_USERS_JSON }}' > $(pwd)/users.json" + - name: Authenticate Docker Engine with GitHub Packages container registry + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.CCASH_DOMAIN }} + username: root + key: ${{ secrets.CCASH_SSH_KEY }} + script: "docker login -u '${{ github.actor }}' -p '${{ secrets.GITHUB_TOKEN }}' docker.pkg.github.com" + - name: Prune docker system + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.CCASH_DOMAIN }} + username: root + key: ${{ secrets.CCASH_SSH_KEY }} + script: "docker system prune -af" - name: Format repository run: | echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV - - name: Build container image - uses: docker/build-push-action@v2 + - name: Pull latest image + uses: appleboy/ssh-action@master with: - push: true - tags: | - docker.pkg.github.com/${{ env.IMAGE_REPOSITORY }}/ccash:${{ github.sha }} - docker.pkg.github.com/${{ env.IMAGE_REPOSITORY }}/ccash:latest + host: ${{ secrets.CCASH_DOMAIN }} + username: root + key: ${{ secrets.CCASH_SSH_KEY }} + script: "docker pull docker.pkg.github.com/${{ env.IMAGE_REPOSITORY }}/ccash:latest" + - name: Stop previous container + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.CCASH_DOMAIN }} + username: root + key: ${{ secrets.CCASH_SSH_KEY }} + script: "curl -X POST -H 'Password: ${{ secrets.CCASH_ADMIN_PASSWORD }}' https://${{ secrets.CCASH_DOMAIN }}/BankF/close && docker kill $(docker ps -q)" + - name: Run CCash Docker image + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.CCASH_DOMAIN }} + username: root + key: ${{ secrets.CCASH_SSH_KEY }} + script: "docker run -d -p 80:80 -p 443:443 -v $(pwd)/config.json:/ccash/config.json -v $(pwd)/users.json:/ccash/users.json -v ${{ secrets.CCASH_TLS_CERT_PATH }}:/ccash/cert -v ${{ secrets.CCASH_TLS_KEY_PATH }}:/ccash/key docker.pkg.github.com/${{ env.IMAGE_REPOSITORY }}/ccash:latest ${{ secrets.CCASH_ADMIN_PASSWORD }} ${{ secrets.CCASH_SAVE_FREQUENCY }} ${{ secrets.CCASH_THREAD_COUNT }}" diff --git a/Dockerfile b/Dockerfile index 8d8c984..13b7fb1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,4 +17,4 @@ ARG SAVE_FREQ=2 RUN ["chmod", "+x", "/CCash/config/ssl.sh"] -CMD ["sh", "-c", "/CCash/config/ssl.sh && /CCash/build/bank ${ADMIN_A} ${SAVE_FREQ}"] +CMD ["sh", "-c", "/CCash/config/ssl.sh && /CCash/build/bank ${ADMIN_A} ${SAVE_FREQ}"] \ No newline at end of file diff --git a/config.json b/config.json index fa95be9..76acb93 100644 --- a/config.json +++ b/config.json @@ -9,8 +9,8 @@ "address": "0.0.0.0", "port": 443, "https": true, - "cert": "", - "key": "" + "cert": "/ccash/cert", + "key": "/ccash/key" } ] } diff --git a/docs/APIs.md b/docs/APIs.md new file mode 100644 index 0000000..360bc3b --- /dev/null +++ b/docs/APIs.md @@ -0,0 +1,11 @@ +# Language Specific APIs + +## Complete +* [JS API](https://github.com/LukeeeeBennett/ccash-client-js) +* [ComputerCraft (Lua) API](https://github.com/Reactified/rpm/blob/main/packages/ccash-api/api.lua) +* [Python API](https://github.com/fearlessdoggo21/ccashpythonclient) + +## In Dev +* [C API]() +* [CS API](https://github.com/Soverclysm/CCash-dotnet-api) +* [Rust API](https://git.stboyden.com/STBoyden/ccash-rs) diff --git a/docs/deploy.md b/docs/deploy.md new file mode 100644 index 0000000..dd32e83 --- /dev/null +++ b/docs/deploy.md @@ -0,0 +1,47 @@ +# Deploying CCash + +CCash can deployed to a remote machine pretty simply. + +A pre-built docker image is supplied in the repos [GitHub Packages](https://github.com/features/packages) container registry [EntireTwix/CCash](https://github.com/EntireTwix/CCash/packages/851105). + +It can be run with docker like so: + +``` +docker pull docker.pkg.github.com/entiretwix/ccash/ccash:latest +``` + +## Build + +The CCash repo provides a GitHub Workflow to build, release and publish the docker image in [.github/workflows/build.yaml](https://github.com/EntireTwix/CCash/blob/main/.github/workflows/build.yaml) to the GitHub Packages container registry. + +You can build and publish your own images using this workflow by forking [EntireTwix/CCash](https://github.com/EntireTwix/CCash). + +## Deploy + +You can deploy this docker image to be run on a remote machine in a few steps. In this case we are using [Debian OS](https://www.debian.org/) running on the [Linode](https://www.linode.com/) cloud provider, but most OS and cloud providers will work, assuming the machine can run an SSH server and Docker. + +Similarly, the CCash repo also provides a GitHub Workflow to deploy the latest docker image to a remote machine in [.github/workflows/deploy.yaml](https://github.com/EntireTwix/CCash/blob/main/.github/workflows/deploy.yaml). + +### Configure the machine + +1. Create a machine using your chosen cloud provider +1. Configure DNS to point your chosen domain name to the machines IP address. _(Without this, TLS/SSL will not work)_ +1. Create an SSH key-pair by running `ssh-keygen` locally. Make sure you **don't** set a password +1. Add the `*.pub` public key to the servers `~/.ssh/authorized_keys` file +1. Install Docker Engine on the remote machine following [official docs](https://docs.docker.com/engine/install/) +1. Generate SSL/TLS certificate (Using [certbot](https://certbot.eff.org/lets-encrypt/debianbuster-other) is recommended) +1. Configure [GitHub secrets](https://docs.github.com/en/actions/reference/encrypted-secrets) for the repo + * `CCASH_SSH_KEY` - The private key _(not `*.pub`)_ created earlier + * `CCASH_DOMAIN` - The domain name pointing to the remote machine + * `CCASH_CONFIG_JSON` - A config.json file that will be written every deploy _(https config cert path should be `/ccash/cert` and key path should be `/ccash/key`)_ + * `CCASH_USERS_JSON` - A users.json file that will be written only on first deploy + * `CCASH_ADMIN_PASSWORD` - A CCash server admin account password + * `CCASH_SAVE_FREQUENCY` - A number representing the frequency to save to users.json (in minutes) + * `CCASH_THREAD_COUNT` - A number representing the number of threads to use + * `CCASH_TLS_CERT_PATH` - The path to the TLS/SSL certificate on the host machine + * `CCASH_TLS_KEY_PATH` - The path to the TLS/SSL key on the host machine + * `CCASH_DEPLOY_TOKEN` - A [GitHub personal access token](https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token), used to trigger the deploy workflow automatically when the build workflow is successful. _(Leave empty to disable automatic deploys.)_ + +You are now ready to run the "Deploy" workflow mentioned above. This workflow will SSH in to the `CCASH_DOMAIN` machine, using the `CCASH_SSH_KEY` and `docker run` the latest `entiretwix/ccash` image, binding to port 80 and 443, setting the appropriate volumes and environment variables. + +Run `curl https://$CCASH_DOMAIN/BankF/ping` to verify that the server has been deployed correctly. diff --git a/docs/help.md b/docs/help.md new file mode 100644 index 0000000..ec7d690 --- /dev/null +++ b/docs/help.md @@ -0,0 +1,45 @@ +# Error Responses + +| # | meaning | +| --- | ----------------- | +| -1 | UserNotFound | +| -2 | WrongPassword | +| -3 | InvalidRequest | +| -4 | NameTooLong | +| -5 | UserAlreadyExists | +| -6 | InsufficientFunds | + +# Things of Note +* all endpoints respond with **JSON** file type +* "**A**" denotes requiring Authentication in the form of a header titled "**Password**" + +# Usage +| Name | Path | Method | A | Description | +| :------------: | :------------------------------------- | :----: | :---: | ------------------------------------------------------------------------------------------------------------------------------- | +| GetBal | BankF/{name}/bal | GET | false | returns the balance of a given user `{name}` | +| GetLog | BankF/{name}/log | GET | true | returns a list of last `n` number of transactions (a configurable amount when the program is compiled) of a given user `{name}` | +| SendFunds | BankF/{name}/send/{to}?amount={amount} | POST | true | sends `{amount}` from user `{name}` to user `{to}` | +| VerifyPassword | BankF/{name}/pass/verify | GET | true | returns `1` if the supplied user `{name}`'s password matches the password supplied in the header | + +# Meta Usage +| Name | Path | Method | A | Description | +| :------------: | :------------------------------------- | :----: | :---: | ---------------------------------------------------------------------------------------------------------------------------------------- | +| ChangePassword | BankF/{name}/pass/change | PATCH | true | if the password supplied in the header matches the user `{name}`'s password, the user's password is changed to the one given in the body | +| SetBal | BankF/admin/{name}/bal?amount={amount} | PATCH | true | sets the balance of a give user `{name}` if the supplied password matches the admin password | + +# System Usage +| Name | Path | Method | A | Description | +| :-------------: | :-------------------- | :----: | :---: | ------------------------------------------------------------------------------------- | +| Help | BankF/help | GET | false | the page you're looking at right now! | +| Ping | BankF/ping | GET | false | for pinging the server to see if its online | +| Close | BankF/admin/close | POST | true | saves and then closes the program if the supplied password matches the admin password | +| Contains | BankF/contains/{name} | GET | false | returns `1` if the supplied user `{name}` exists | +| AdminVerifyPass | BankF/admin/verify | GET | true | returns `1` if the password supplied in the header matches the admin password | + +# User Management +| Name | Path | Method | A | Description | +| :----------: | :------------------------------------------ | :----: | :---: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| AddUser | BankF/user/{name} | POST | true | registers a user with the name `{name}`, balance of 0 and a password of the password supplied in the header | +| AdminAddUser | BankF/admin/user/{name}?init_bal={init_bal} | POST | true | if the password supplied in the header matches the admin password, then it registers a user with the name `{name}`, balance of `init_bal` and a password supplied by the body of the request | +| DelUser | BankF/user/{name} | DELETE | true | if the password supplied in the header matches the user `{name}`'s password, then the user is deleted | +| AdminDelUser | BankF/admin/user/{name} | DELETE | true | if the password supplied in the header matches the admin password, then the user is deleted | diff --git a/docs/services.md b/docs/services.md new file mode 100644 index 0000000..e570210 --- /dev/null +++ b/docs/services.md @@ -0,0 +1,29 @@ +# Connected Services + +### Implemented: + +- [Web Frontend](https://github.com/Expand-sys/ccashfrontend) + ![image](https://user-images.githubusercontent.com/31377881/121337724-afe9fe80-c8d1-11eb-8851-23ec5e74cd26.png) +- [CC Frontend](https://github.com/Reactified/rpm/blob/main/packages/ccash-wallet) + + ![image](https://user-images.githubusercontent.com/31377881/121338034-fb041180-c8d1-11eb-8640-b18c141eb980.png) +- [CC Shop](https://github.com/Reactified/rpm/tree/main/packages/ccash-shop) + ![image](https://user-images.githubusercontent.com/31377881/120050327-de163700-bfd1-11eb-9d5a-f75c003e867c.png) + ![image](https://user-images.githubusercontent.com/31377881/120050367-09992180-bfd2-11eb-9a22-449d73c196cf.png) +- [CC Reverse ATM](https://github.com/Reactified/misc/tree/main/lua/ccash-bank) an ATM for economies allowing for an initial exchange to start up + ![image](https://user-images.githubusercontent.com/31377881/121277361-4d6b1100-c885-11eb-87c8-cfebcf58da4f.png) + +### In-Dev: + +- [a Market](https://github.com/STBoyden/market-api-2.0) + +### Ideas: + +- an API in your preferred language (if not found in [APIs.md](APIs.md)) +- Gambling +- Shipping +- High-level bank operations such as loans +- Some trust based system for transactions similiar to Paypal + +- a better version of one of these existing ideas +- something completely different diff --git a/src/bank.cpp b/src/bank.cpp index 0fed395..bfdffb4 100644 --- a/src/bank.cpp +++ b/src/bank.cpp @@ -50,7 +50,8 @@ size_t Bank::SumBal() const noexcept BankResponse Bank::GetBal(const std::string &name) const noexcept { uint32_t res = 0; - if (!ValidUsername(name) || !users.if_contains(name, [&res](const User &u) { res = u.balance; })) + if (!ValidUsername(name) || !users.if_contains(name, [&res](const User &u) + { res = u.balance; })) { return {k404NotFound, "\"User not found\""}; } @@ -63,7 +64,8 @@ BankResponse Bank::GetBal(const std::string &name) const noexcept BankResponse Bank::GetLogs(const std::string &name) noexcept { BankResponse res; - if (!users.modify_if(name, [&res](User &u) { res = {k200OK, u.log.GetLogs()}; })) + if (!users.modify_if(name, [&res](User &u) + { res = {k200OK, u.log.GetLogs()}; })) { return {k404NotFound, "\"User not found\""}; } @@ -93,33 +95,36 @@ BankResponse Bank::SendFunds(const std::string &a_name, const std::string &b_nam #if MAX_LOG_SIZE > 0 time_t current_time = time(NULL); #endif - if (!users.modify_if(a_name, [current_time, &a_name, &b_name, &res, amount](User &a) { - //if A can afford it - if (a.balance < amount) - { - res = {k400BadRequest, "\"Insufficient funds\""}; - } - else - { - a.balance -= amount; + if (!users.modify_if(a_name, [current_time, &a_name, &b_name, &res, amount](User &a) + { + //if A can afford it + if (a.balance < amount) + { + res = {k400BadRequest, "\"Insufficient funds\""}; + } + else + { + a.balance -= amount; #if MAX_LOG_SIZE > 0 - a.log.AddTrans(a_name, b_name, amount, current_time); + a.log.AddTrans(a_name, b_name, amount, current_time); #endif - res = {k200OK, std::to_string(a.balance)}; - } - })) + res = {k200OK, std::to_string(a.balance)}; + } + })) { return {k404NotFound, "\"Sender does not exist\""}; } if (res.first == k200OK) { #if MAX_LOG_SIZE > 0 - users.modify_if(b_name, [current_time, &a_name, &b_name, amount](User &b) { - b.balance += amount; - b.log.AddTrans(a_name, b_name, amount, current_time); - }); + users.modify_if(b_name, [current_time, &a_name, &b_name, amount](User &b) + { + b.balance += amount; + b.log.AddTrans(a_name, b_name, amount, current_time); + }); #else - users.modify_if(b_name, [amount](User &b) { b.balance += amount; }); + users.modify_if(b_name, [amount](User &b) + { b.balance += amount; }); #endif #if CONSERVATIVE_DISK_SAVE #if MULTI_THREADED @@ -134,7 +139,8 @@ BankResponse Bank::SendFunds(const std::string &a_name, const std::string &b_nam bool Bank::VerifyPassword(const std::string &name, const std::string_view &attempt) const noexcept { bool res = false; - users.if_contains(name, [&res, &attempt](const User &u) { res = (u.password == xxHashStringGen{}(attempt)); }); + users.if_contains(name, [&res, &attempt](const User &u) + { res = (u.password == xxHashStringGen{}(attempt)); }); return res; } @@ -147,11 +153,13 @@ void Bank::ChangePassword(const std::string &name, const std::string &new_pass) save_flag = true; #endif #endif - users.modify_if(name, [&new_pass](User &u) { u.password = xxHashStringGen{}(new_pass); }); + users.modify_if(name, [&new_pass](User &u) + { u.password = xxHashStringGen{}(new_pass); }); } BankResponse Bank::SetBal(const std::string &name, uint32_t amount) noexcept { - if (ValidUsername(name) && users.modify_if(name, [amount](User &u) { u.balance = amount; })) + if (ValidUsername(name) && users.modify_if(name, [amount](User &u) + { u.balance = amount; })) { #if CONSERVATIVE_DISK_SAVE #if MULTI_THREADED @@ -174,7 +182,8 @@ BankResponse Bank::ImpactBal(const std::string &name, int64_t amount) noexcept return {k400BadRequest, "\"Amount cannot be 0\""}; } uint32_t balance; - if (ValidUsername(name) && users.modify_if(name, [&balance, amount](User &u) { balance = (u.balance < (amount * -1) ? u.balance = 0 : u.balance += amount); })) + if (ValidUsername(name) && users.modify_if(name, [&balance, amount](User &u) + { balance = (u.balance < (amount * -1) ? u.balance = 0 : u.balance += amount); })) { #if CONSERVATIVE_DISK_SAVE #if MULTI_THREADED @@ -226,7 +235,8 @@ BankResponse Bank::DelUser(const std::string &name) noexcept { #if RETURN_ON_DEL uint32_t bal; - if (users.if_contains(name, [&bal](const User &u) { bal = u.balance; }) && + if (users.if_contains(name, [&bal](const User &u) + { bal = u.balance; }) && bal) { users.modify_if(return_account, [bal](User & u)) @@ -257,7 +267,8 @@ void Bank::DelSelf(const std::string &name) noexcept { #if RETURN_ON_DEL uint32_t bal; - if (users.if_contains(name, [&bal](const User &u) { bal = u.balance; }) && + if (users.if_contains(name, [&bal](const User &u) + { bal = u.balance; }) && bal) { users.modify_if(return_account, [bal](User & u)) @@ -302,10 +313,11 @@ const char *Bank::Save() for (const auto &u : users) { //we know it contains this key but we call this func to grab mutex - users.if_contains(u.first, [&users_copy, &u](const User &u_val) { - users_copy.users.emplace_back(u_val.Encode()); - users_copy.keys.emplace_back(u.first); - }); + users.if_contains(u.first, [&users_copy, &u](const User &u_val) + { + users_copy.users.emplace_back(u_val.Encode()); + users_copy.keys.emplace_back(u.first); + }); } } FBE::bank_dom::GlobalFinalModel writer; @@ -335,7 +347,6 @@ const char *Bank::Save() } #endif } - //NOT THREAD SAFE, BY NO MEANS SHOULD THIS BE CALLED WHILE RECEIEVING REQUESTS void Bank::Load() {