Compare commits

3 Commits

Author SHA1 Message Date
dc62117d7a [mod] modified readme to include query string support
- considered experimental until more test coverage is done
2021-03-07 19:45:01 -05:00
297016e799 [add] added query string support 2021-03-07 19:11:49 -05:00
c0a05f653c [mod] modified readme
- removed zero width spaces
 - added some more examples and roadmap
2021-03-07 18:41:54 -05:00
5 changed files with 108 additions and 35 deletions

View File

@@ -4,7 +4,7 @@
A simplistic Dynamic DNS-ish server. A simplistic Dynamic DNS-ish server.
</h4> </h4>
`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`). `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`). No special client side application needed, just something to do a `POST` request like `wget`, `curl` or `httpie`.
<p align="center"> <p align="center">
<strong> <strong>
@@ -12,6 +12,7 @@
• <a href="#getting-started">Getting started</a> • <a href="#getting-started">Getting started</a>
• <a href="#rest-api">Rest API</a> • <a href="#rest-api">Rest API</a>
• <a href="#configuration">Configuration</a> • <a href="#configuration">Configuration</a>
• <a href="#roadmap">Roadmap</a>
</strong> </strong>
</p> </p>
@@ -43,38 +44,75 @@ JSON_REQUEST_BODY='{
"public_ip": false "public_ip": false
}' }'
http post $API_SERVER/admin/client $API_KEY <<< $JSON_REQUEST_BODY http post $API_SERVER/admin/client $API_KEY <<< $JSON_REQUEST_BODY
# Alternatively using query strings (be sure to encode special characters in the param values)
API_KEY='X-API-Key=MdSUxrQQ.d5d40cd9.e2s1BsQEqmSE5MqyHYbDBVxGr/KY4BYIV5QkdjiCOF0='
CLIENT_PARAMS='name=clientA&description=Client+across+the+river&permission=rw&public_ip=false'
http post $API_SERVER/admin/client?$API_KEY&$CLIENT_PARAMS
```
Updating a write client's `ipv4`:
```bash
# using httpie
API_SERVER=http://localhost:3000/rynDNS
API_KEY=X-API-Key:zW3If4s5.3d6129cc.48d41d2a28b16a9a274d3f407026aee2b82cddbb3c11a5496ffe329c78e5d54b
# automatically set ip based on request itself
http put $API_SERVER/client $API_KEY
# set a custom ip for the client
JSON_REQUEST_BODY='{
"ipv4": "115.16.0.99"
}'
http put $API_SERVER/client $API_KEY <<< $JSON_REQUEST_BODY
```
Reading a write client's `ipv4` with a read client:
```bash
# using httpie
API_SERVER=http://localhost:3000/rynDNS
API_KEY=X-API-Key:zW3If4s5.28aef5cb.d502cb94ba0802c6363f95ecb3a70de7e2e4b889779e435b7c033f2c72ed4f20
CLIENT_NAME=write_client
http get $API_SERVER/clients/$CLIENT_NAME $ API_KEY
# returns clients name and ipv4 as json
#{
# "name": "write_client",
# "ipv4": "115.16.0.99"
#}
``` ```
## REST API ## 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`. 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`.
All requests except for `/public/*` require a valid `X-API-Key` header with appropriate permission. All requests except for `/public/*` require a valid `X-API-Key` header with appropriate permission.
**Experimental:** All parameters (including `X-API-Key`) can be passed as query string.
The following is a quick overview of the available commands. The following is a quick overview of the available commands.
### Admin ### Admin
Permission `x` Permission `x`
* [`POST /admin/client`](https://demo.api.pdev.dev/#/admin/create_client) - Create or update a client and retrieve a new API key * [`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 * [`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`](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 * [`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 * [`DELETE /admin/clients/{client_name}`](https://demo.api.pdev.dev/#/admin/delete_client) - Delete an existing client
### Client (write) ### Client (write)
Permission `w` 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 * [`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 * [`DELETE /client`](https://demo.api.pdev.dev/#/client%20(write)/delete_client_ip) - Delete the requesting client's ipv4 information
### Client (read) ### Client (read)
Permission `r` Permission `r`
* [`GET /clients/{client_name}`](https://demo.api.pdev.dev/#/client%20(read)/get_client_ip) - Get ipv4 client information * [`GET /clients/{client_name}`](https://demo.api.pdev.dev/#/client%20(read)/get_client_ip) - Get ipv4 client information
### Public ### Public
Client attribute `public_ip: true` Client attribute `public_ip: true`
* [`GET /public/{client_name}`](https://demo.api.pdev.dev/#/public/get_public_client_ip) - Get ipv4 client information * [`GET /public/{client_name}`](https://demo.api.pdev.dev/#/public/get_public_client_ip) - Get ipv4 client information
## Configuration ## Configuration
@@ -96,3 +134,11 @@ RAILS_API_DEMO: 1
# [DOCKER only] set cycle to reset demo data [default: 60m] # [DOCKER only] set cycle to reset demo data [default: 60m]
RAILS_DEMO_RESET: 60m RAILS_DEMO_RESET: 60m
``` ```
## Roadmap
* `Public Mode` - to allow for quick deployment without having to create clients/api-keys in a trusted environment (see #1)
* ~~Support Query Strings - as alternative to api key in header and json body~~ (merged 297016e799)
* add full test coverage for query strings (considered experimental until then)
* add query strings to `api.yml` and additional examples
* DNS Server in Docker container - resolve `client_name`s via DNS server

View File

@@ -178,10 +178,12 @@ class ClientsController < ApplicationController
# check if the api is valid and has the appropriate prefix # check if the api is valid and has the appropriate prefix
def parse_api_key_header def parse_api_key_header
# TODO: move api_key checks and formatting to helper class # TODO: move api_key checks and formatting to helper class
# check if api key is present and can be split into 3 parts: <prefix>.<name_id>.<api_key> # check if api key is present in header or param and can be split into 3 parts: <prefix>.<name_id>.<api_key>
# NOTE: .compact is more for got measure than really necessary, but this ensures no nil values in the array # NOTE: .compact is more for got measure than really necessary, but this ensures no nil values in the array
api_key_header = request.headers[RynDNS::Application::API_KEY_HEADER].split('.').compact unless api_key_header = request.headers[RynDNS::Application::API_KEY_HEADER].split('.').compact unless
request.headers[RynDNS::Application::API_KEY_HEADER].nil? request.headers[RynDNS::Application::API_KEY_HEADER].nil?
api_key_header ||= params[RynDNS::Application::API_KEY_HEADER].split('.').compact if
params.has_key?(RynDNS::Application::API_KEY_HEADER)
return head(401) if api_key_header.nil? || api_key_header.length < 3 return head(401) if api_key_header.nil? || api_key_header.length < 3
prefix, @name_id, @api_key = api_key_header prefix, @name_id, @api_key = api_key_header

View File

@@ -12,7 +12,7 @@ RSpec.describe 'Clients', type: :request do
let(:valid_client) do let(:valid_client) do
{ {
name: 'clientA', name: 'clientA',
description: 'Client across the river', description: 'Client&across the=river', # added &= for query string test
permission: 'rw', permission: 'rw',
public_ip: false public_ip: false
} }
@@ -78,31 +78,18 @@ RSpec.describe 'Clients', type: :request do
context 'when sending a valid request' do context 'when sending a valid request' do
before { post '/admin/client', params: valid_client, headers: api_key_header(admin_client) } before { post '/admin/client', params: valid_client, headers: api_key_header(admin_client) }
it 'creates client that can read/write and returns 201' do it_behaves_like 'POST /admin/client 201'
expect(json_response).not_to be_empty end
expect(json_response['name']).to eq valid_client[:name]
expect(json_response['api_key'].length).to eq 82 # TODO: add query string tests for all requests and use shared examples
expect(response).to have_http_status(201) context 'when sending a valid request as query string' do
api_key = json_response['api_key'] before do
# check if request was parsed properly api_key = "#{RynDNS::Application::API_KEY_HEADER}=#{api_key_header(admin_client)[RynDNS::Application::API_KEY_HEADER]}"
get "/admin/clients/#{valid_client[:name]}", headers: api_key_header(admin_client) body = hash_to_query_string(valid_client)
expect(json_response).not_to be_empty post "/admin/client?#{api_key}&#{body}"
expect(json_response['name']).to eq valid_client[:name]
expect(json_response['description']).to eq valid_client[:description]
expect(json_response['permission']).to eq valid_client[:permission]
expect(json_response['public_ip']).to eq valid_client[:public_ip]
expect(response).to have_http_status(200)
# test the new client write
put '/client', headers: api_key_header(nil, api_key)
expect(json_response).not_to be_empty
expect(response).to have_http_status(200)
ipv4 = json_response['ipv4']
# test the new client read
get "/clients/#{valid_client[:name]}", headers: api_key_header(nil, api_key)
expect(json_response).not_to be_empty
expect(response).to have_http_status(200)
expect(json_response['ipv4']).to eq ipv4
end end
it_behaves_like 'POST /admin/client 201'
end end
context 'when sending a valid request without api_key' do context 'when sending a valid request without api_key' do

View File

@@ -15,6 +15,15 @@ module RequestSpecHelper
"#{'r' if perm_r}#{'w' if perm_w}#{'x' if perm_x}" "#{'r' if perm_r}#{'w' if perm_w}#{'x' if perm_x}"
end end
def hash_to_query_string(hash)
result = ''
hash.each do |k, v|
# escape value to be uri save
result += "#{k}=#{CGI.escape(v.to_s)}&"
end
result.delete_suffix('&')
end
def list_equals_db?(client_list) def list_equals_db?(client_list)
return false unless Client.count == client_list.count return false unless Client.count == client_list.count

View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true
RSpec.shared_examples 'POST /admin/client 201' do
it 'creates client that can read/write and returns 201' do
expect(json_response).not_to be_empty
expect(json_response['name']).to eq valid_client[:name]
expect(json_response['api_key'].length).to eq 82
expect(response).to have_http_status(201)
api_key = json_response['api_key']
# check if request was parsed properly
get "/admin/clients/#{valid_client[:name]}", headers: api_key_header(admin_client)
expect(json_response).not_to be_empty
expect(json_response['name']).to eq valid_client[:name]
expect(json_response['description']).to eq valid_client[:description]
expect(json_response['permission']).to eq valid_client[:permission]
expect(json_response['public_ip']).to eq valid_client[:public_ip]
expect(response).to have_http_status(200)
# test the new client write
put '/client', headers: api_key_header(nil, api_key)
expect(json_response).not_to be_empty
expect(response).to have_http_status(200)
ipv4 = json_response['ipv4']
# test the new client read
get "/clients/#{valid_client[:name]}", headers: api_key_header(nil, api_key)
expect(json_response).not_to be_empty
expect(response).to have_http_status(200)
expect(json_response['ipv4']).to eq ipv4
end
end