Compare commits
4 Commits
5868377210
...
public_mod
| Author | SHA1 | Date | |
|---|---|---|---|
|
c4cb76fe0b
|
|||
|
8bbd3e02f3
|
|||
|
a0ff5cdebb
|
|||
|
d5b1006d2a
|
100
README.md
100
README.md
@@ -1,24 +1,98 @@
|
|||||||
# README
|
<h1 align="center">rynDNS</h1>
|
||||||
|
|
||||||
This README would normally document whatever steps are necessary to get the
|
<h4 align="center">
|
||||||
application up and running.
|
A simplistic Dynamic DNS-ish server.
|
||||||
|
</h4>
|
||||||
|
|
||||||
Things you may want to cover:
|
`rynDNS` is a simple REST-API for `name` to `ip` resolving in the spirit of Dynamic DNS. Clients can `PUT` their `ipv4` address up to be read by other clients. Access is controlled via `api_key` and permissions (`read`, `write`, `admin`).
|
||||||
|
|
||||||
* Ruby version
|
<p align="center">
|
||||||
|
<strong>
|
||||||
|
<a href="https://demo.api.pdev.dev">Demo</a>
|
||||||
|
• <a href="#getting-started">Getting started</a>
|
||||||
|
• <a href="#rest-api">Rest API</a>
|
||||||
|
• <a href="#configuration">Configuration</a>
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
* System dependencies
|
## Getting Started
|
||||||
|
|
||||||
* Configuration
|
This repo includes a [`Dockerfile`](Dockerfile) with an accompanying [`docker-compose.yml`](docker-compose.yml). To build and launch the container simply run:
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
Once the container is build and running it will create a default admin account and post its randomly generated api key to the console:
|
||||||
|
```
|
||||||
|
ryndns | No admin accounts in Database, adding default: admin
|
||||||
|
ryndns | Created default admin with api_key: 3NJbP4tS.39b92576.062...
|
||||||
|
```
|
||||||
|
The API is now accessible at http://localhost:3000/rynDNS.
|
||||||
|
```bash
|
||||||
|
# using httpie
|
||||||
|
API_SERVER=http://localhost:3000/rynDNS
|
||||||
|
API_KEY=X-API-Key:3NJbP4tS.39b92576.062142e6451df545d5034ca8f6050b80ff3e9689f20d5e45d72d160fa7e1cf32
|
||||||
|
|
||||||
* Database creation
|
# list all clients
|
||||||
|
http get $API_SERVER/admin/clients $API_KEY
|
||||||
|
|
||||||
* Database initialization
|
# creating a client
|
||||||
|
JSON_REQUEST_BODY='{
|
||||||
|
"name": "clientA",
|
||||||
|
"description": "Client across the river",
|
||||||
|
"permission": "rw",
|
||||||
|
"public_ip": false
|
||||||
|
}'
|
||||||
|
http post $API_SERVER/admin/client $API_KEY <<< $JSON_REQUEST_BODY
|
||||||
|
```
|
||||||
|
## REST API
|
||||||
|
A detailed and interactive specification can be found in the [demo](https://demo.api.pdev.dev) which is a render of the OpenAPI specification [api.yml](api.yml) with `swager-ui`.
|
||||||
|
|
||||||
* How to run the test suite
|
All requests except for `/public/*` require a valid `X-API-Key` header with appropriate permission.
|
||||||
|
|
||||||
* Services (job queues, cache servers, search engines, etc.)
|
The following is a quick overview of the available commands.
|
||||||
|
|
||||||
* Deployment instructions
|
### Admin
|
||||||
|
Permission `x`
|
||||||
|
|
||||||
* ...
|
* [`POST /admin/client`](https://demo.api.pdev.dev/#/admin/create_client) - Create or update a client and retrieve a new API key
|
||||||
|
* [`PUT /admin/client`](https://demo.api.pdev.dev/#/admin/update_client) - Update an existing client
|
||||||
|
* [`GET /admin/clients`](https://demo.api.pdev.dev/#/admin/get_all_client) - Retrieve all clients information
|
||||||
|
* [`GET /admin/clients/{client_name}`](https://demo.api.pdev.dev/#/admin/get_client) - Retrieve client information
|
||||||
|
* [`DELETE /admin/clients/{client_name}`](https://demo.api.pdev.dev/#/admin/delete_client) - Delete an existing client
|
||||||
|
|
||||||
|
### Client (write)
|
||||||
|
Permission `w`
|
||||||
|
|
||||||
|
* [`PUT /client`](https://demo.api.pdev.dev/#/client%20(write)/update_client_ip) - Update ipv4 client information based on request IP or body if present
|
||||||
|
* [`DELETE /client`](https://demo.api.pdev.dev/#/client%20(write)/delete_client_ip) - Delete the requesting client's ipv4 information
|
||||||
|
|
||||||
|
### Client (read)
|
||||||
|
Permission `r`
|
||||||
|
|
||||||
|
* [`GET /clients/{client_name}`](https://demo.api.pdev.dev/#/client%20(read)/get_client_ip) - Get ipv4 client information
|
||||||
|
|
||||||
|
### Public
|
||||||
|
Client attribute `public_ip: true`
|
||||||
|
|
||||||
|
* [`GET /public/{client_name}`](https://demo.api.pdev.dev/#/public/get_public_client_ip) - Get ipv4 client information
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The `rynDNS` server can be configured with environment variables, these can also be set in the docker environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# set to run app in subdirectory [default: /]
|
||||||
|
RAILS_RELATIVE_URL_ROOT: /rynDNS
|
||||||
|
# set maximum number of clients in db [default: 250]
|
||||||
|
RAILS_MAX_CLIENTS: 250
|
||||||
|
# set custom api key prefix [default: randomly generated (saved and restored if using docker)]
|
||||||
|
API_KEY_PREFIX: zW3If4s5
|
||||||
|
# set custom header name for api key [default: X-API-Key]
|
||||||
|
API_KEY_HEADER: X-API-Key
|
||||||
|
# set to reset the admin account, this will output a new api key on the console when starting the server [default: 0]
|
||||||
|
RAILS_RESET_ADMIN: 1
|
||||||
|
# set app to run as demo [default: 0]
|
||||||
|
RAILS_API_DEMO: 1
|
||||||
|
# [DOCKER only] set cycle to reset demo data [default: 60m]
|
||||||
|
RAILS_DEMO_RESET: 60m
|
||||||
|
```
|
||||||
35
api.yml
35
api.yml
@@ -253,6 +253,39 @@ paths:
|
|||||||
$ref: "#/components/schemas/client_info"
|
$ref: "#/components/schemas/client_info"
|
||||||
"401":
|
"401":
|
||||||
$ref: "#/components/responses/unauthorized_public"
|
$ref: "#/components/responses/unauthorized_public"
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "public"
|
||||||
|
summary: "Set ipv4 client information. This is ONLY available if ENV['RAILS_API_PUBLIC'] is set"
|
||||||
|
description: "If body is present, it will take precedence over the request IP. Response contains the newly set `ipv4` value."
|
||||||
|
requestBody:
|
||||||
|
description: "An existing clients properties wil only be updated if the parameter is present."
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/client_update"
|
||||||
|
operationId: "set_public_client_ip"
|
||||||
|
security: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: "Client created"
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/client_info"
|
||||||
|
"201":
|
||||||
|
description: "Client updated"
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/client_info"
|
||||||
|
"403":
|
||||||
|
description: "Forbidden - API not public"
|
||||||
|
"406":
|
||||||
|
description: "Not Acceptable - IPv4 Address is invalid"
|
||||||
|
"429":
|
||||||
|
description: "Too many clients - Maximum client count reached"
|
||||||
|
|
||||||
components:
|
components:
|
||||||
|
|
||||||
@@ -382,4 +415,4 @@ components:
|
|||||||
pattern: "^(((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(.(?!$)|$)){4})?$"
|
pattern: "^(((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(.(?!$)|$)){4})?$"
|
||||||
externalDocs:
|
externalDocs:
|
||||||
description: "<rynDNS source code>"
|
description: "<rynDNS source code>"
|
||||||
url: "https://git.pdev.dev/pdev/ryndns"
|
url: "https://git.pdev.dev/pascal/ryndns"
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
require 'resolv'
|
require 'resolv'
|
||||||
|
|
||||||
class ClientsController < ApplicationController
|
class ClientsController < ApplicationController
|
||||||
before_action :parse_api_key_header, except: [:show_public_ipv4]
|
before_action :parse_api_key_header, except: %i[show_public_ipv4 create_public_ipv4]
|
||||||
before_action :authenticate_client, except: [:show_public_ipv4]
|
before_action :authenticate_client, except: %i[show_public_ipv4 create_public_ipv4]
|
||||||
before_action :authorize_perm_r, only: [:show_ipv4]
|
before_action :authorize_perm_r, only: [:show_ipv4]
|
||||||
before_action :authorize_perm_w, only: %i[update_ipv4 destroy_ipv4]
|
before_action :authorize_perm_w, only: %i[update_ipv4 destroy_ipv4]
|
||||||
before_action :authorize_perm_x, only: %i[create update index show destroy]
|
before_action :authorize_perm_x, only: %i[create update index show destroy]
|
||||||
|
before_action :authorize_public, only: [:create_public_ipv4] if ENV['RAILS_API_PUBLIC']
|
||||||
before_action :client_authorized, except: [:show_public_ipv4]
|
before_action :client_authorized, except: [:show_public_ipv4]
|
||||||
before_action :demo, only: %i[create update destroy] if ENV['RAILS_API_DEMO']
|
before_action :demo, only: %i[create update destroy] if ENV['RAILS_API_DEMO']
|
||||||
|
|
||||||
@@ -129,6 +130,29 @@ class ClientsController < ApplicationController
|
|||||||
json_response(client_read_object(client))
|
json_response(client_read_object(client))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# POST /public/:id
|
||||||
|
def create_public_ipv4
|
||||||
|
ipv4 = request_ip
|
||||||
|
|
||||||
|
# return 406 if body with invalid ipv4 is present
|
||||||
|
# TODO: move this into the model (update)
|
||||||
|
return head(406) unless ipv4
|
||||||
|
|
||||||
|
client_params = params.permit(:name)
|
||||||
|
client = Client.find_by name: params[:name]
|
||||||
|
status_code = client ? 200 : 201
|
||||||
|
|
||||||
|
# create new client or update existing
|
||||||
|
if client.nil?
|
||||||
|
client ||= Client.new_client(client_params)
|
||||||
|
return head(429) if client.nil? # reached max client count
|
||||||
|
end
|
||||||
|
client.public_ip = true
|
||||||
|
client.ipv4 = ipv4
|
||||||
|
client.save
|
||||||
|
json_response(client_read_object(client), status_code)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# helper methods
|
# helper methods
|
||||||
@@ -215,6 +239,10 @@ class ClientsController < ApplicationController
|
|||||||
@authorized = @requesting_client.admin?
|
@authorized = @requesting_client.admin?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def authorize_public
|
||||||
|
@authorized = true
|
||||||
|
end
|
||||||
|
|
||||||
# check if request is authorized
|
# check if request is authorized
|
||||||
def client_authorized
|
def client_authorized
|
||||||
return head(403) unless @authorized
|
return head(403) unless @authorized
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Rails.application.routes.draw do
|
|||||||
# separate public path to allow for easier segregation and controls via reverse proxy
|
# separate public path to allow for easier segregation and controls via reverse proxy
|
||||||
scope '/public' do
|
scope '/public' do
|
||||||
get ':name', to: 'clients#show_public_ipv4', format: false
|
get ':name', to: 'clients#show_public_ipv4', format: false
|
||||||
|
post ':name', to: 'clients#create_public_ipv4', format: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
0
data/.keep
Normal file
0
data/.keep
Normal file
@@ -5,8 +5,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
USER_UID: ${UID_PROXY:-12001}
|
USER_UID: ${UID_PROXY:-$EUID}
|
||||||
USER_GID: ${UID_PROXY:-12001}
|
USER_GID: ${UID_PROXY:-$EUID}
|
||||||
environment:
|
environment:
|
||||||
# set to run app in subdirectory
|
# set to run app in subdirectory
|
||||||
RAILS_RELATIVE_URL_ROOT: /rynDNS
|
RAILS_RELATIVE_URL_ROOT: /rynDNS
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
prefix_file="${API_PREFIX_FILE:-/app/data/api_prefix}"
|
prefix_file="${API_PREFIX_FILE:-/app/data/api_prefix}"
|
||||||
db_file="${RAILS_DB_PATH:-/app/data/rynDNS.sqlite3}"
|
db_file="${RAILS_DB_PATH:-/app/data/rynDNS.sqlite3}"
|
||||||
default_admin_file="${API_DEFAULT_KEY_FILE:-/app/data/api_default_admin}"
|
default_admin_file="${API_DEFAULT_KEY_FILE:-/app/data/api_default_admin}"
|
||||||
|
server_pid_file="${RAILS_PID_FILE:-/app/tmp/pids/server.pid}"
|
||||||
rails_bin="${RAILS_BINARY:-/app/bin/rails}"
|
rails_bin="${RAILS_BINARY:-/app/bin/rails}"
|
||||||
demo_cycle="${RAILS_DEMO_RESET:-60m}"
|
demo_cycle="${RAILS_DEMO_RESET:-60m}"
|
||||||
demo_prefix="zW3If4s5"
|
demo_prefix="zW3If4s5"
|
||||||
@@ -50,6 +51,15 @@ function random_sting()
|
|||||||
echo -n "$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w "${1:-32}" | head -n 1)"
|
echo -n "$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w "${1:-32}" | head -n 1)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanup()
|
||||||
|
{
|
||||||
|
if [ -f $server_pid_file ]; then
|
||||||
|
log WAR cleanup last server shutdown may have not been graceful
|
||||||
|
log cleanup deleting old server pid file
|
||||||
|
rm $server_pid_file
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
function get_prefix_file()
|
function get_prefix_file()
|
||||||
{
|
{
|
||||||
if [ -f "$prefix_file" ]; then
|
if [ -f "$prefix_file" ]; then
|
||||||
@@ -113,6 +123,7 @@ function db_setup()
|
|||||||
|
|
||||||
function main()
|
function main()
|
||||||
{
|
{
|
||||||
|
cleanup
|
||||||
set_api_prefix
|
set_api_prefix
|
||||||
log main prefix: $RAILS_API_KEY_PREFIX
|
log main prefix: $RAILS_API_KEY_PREFIX
|
||||||
db_setup
|
db_setup
|
||||||
|
|||||||
@@ -587,4 +587,49 @@ RSpec.describe 'Clients', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'POST /public/:name' do
|
||||||
|
# FIXME: rather have this automated than adding ENV by hand
|
||||||
|
if ENV['RAILS_API_PUBLIC']
|
||||||
|
|
||||||
|
context 'when request to write public client' do
|
||||||
|
before { post "/public/#{public_client.name}" }
|
||||||
|
it 'returns status code 200' do
|
||||||
|
expect(json_response['name']).to eq public_client.name
|
||||||
|
expect(json_response['ipv4']).to eq '127.0.0.1'
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when requesting to write public client with body' do
|
||||||
|
before { post "/public/#{public_client.name}", params: { ipv4: '99.55.66.2' } }
|
||||||
|
it 'returns status code 200' do
|
||||||
|
expect(json_response['name']).to eq public_client.name
|
||||||
|
expect(json_response['ipv4']).to eq '99.55.66.2'
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when requesting to write non existing client' do
|
||||||
|
before { post "/public/#{non_existing_client}" }
|
||||||
|
it 'returns status code 201' do
|
||||||
|
expect(json_response['name']).to eq non_existing_client
|
||||||
|
expect(json_response['ipv4']).to eq '127.0.0.1'
|
||||||
|
expect(response).to have_http_status(201)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# TODO: 406 and 429, check client count on 200
|
||||||
|
else
|
||||||
|
|
||||||
|
context 'when request to write public client' do
|
||||||
|
before { post "/public/#{public_client.name}" }
|
||||||
|
it 'returns status code 403' do
|
||||||
|
expect(response.body).to be_empty
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# TODO: check client count/modification
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user