This commit is contained in:
parent
c2b0f9a3eb
commit
ac6145392c
21
.drone.yml
21
.drone.yml
@ -1,17 +1,14 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: exec
|
||||
name: just a test
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
workspace:
|
||||
base: /go
|
||||
path: src/dn42regsrv
|
||||
|
||||
steps:
|
||||
- name: environment
|
||||
- name: build
|
||||
image: golang
|
||||
commands:
|
||||
- pwd
|
||||
- env
|
||||
- df -h
|
||||
- ls -la
|
||||
- ls -la ../
|
||||
- id
|
||||
- ps -ef
|
||||
|
||||
|
||||
- go get
|
||||
|
500
API.md
Normal file
500
API.md
Normal file
@ -0,0 +1,500 @@
|
||||
# dn42regsrv API Description
|
||||
|
||||
## Registry API
|
||||
|
||||
The general form of the registry query API is:
|
||||
|
||||
```
|
||||
GET /api/registry/{type}/{object}/{key}/{attribute}?raw
|
||||
```
|
||||
|
||||
* Prefixing with a '*' performs a case insensitive, substring match
|
||||
* A '*' on its own means match everything
|
||||
* Otherwise an exact, case sensitive match is performed
|
||||
|
||||
By default, results are returned as JSON objects, and the registry data is decorated
|
||||
with markdown style links depending on relations defined in the DN42 schema. For object
|
||||
results, a 'Backlinks' section is also added providing an array of registry objects that
|
||||
reference this one.
|
||||
|
||||
If the 'raw' parameter is provided, attributes are returned un-decorated exactly
|
||||
as contained in the registry.
|
||||
|
||||
Some examples will help clarify:
|
||||
|
||||
* Return a JSON object, with keys for each registry type and values containing a count
|
||||
of the number of registry objects for each type
|
||||
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/registry/ | jq
|
||||
{
|
||||
"as-block": 8,
|
||||
"as-set": 34,
|
||||
"aut-num": 1486,
|
||||
"domain": 451,
|
||||
"inet6num": 746,
|
||||
"inetnum": 1276,
|
||||
"key-cert": 7,
|
||||
"mntner": 1379,
|
||||
"organisation": 275,
|
||||
"person": 1388,
|
||||
"registry": 4,
|
||||
"role": 14,
|
||||
"route": 892,
|
||||
"route-set": 2,
|
||||
"route6": 596,
|
||||
"schema": 18,
|
||||
"tinc-key": 25,
|
||||
"tinc-keyset": 3
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
* Return a list of all objects in the role type
|
||||
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/registry/role | jq
|
||||
{
|
||||
"role": [
|
||||
"ORG-NETRAVNEN-DN42",
|
||||
"PACKETPUSHERS-DN42",
|
||||
"CCCKC-DN42",
|
||||
"FLHB-ABUSE-DN42",
|
||||
"NIXNODES-DN42",
|
||||
"ORG-SHACK-ABUSE-DN42",
|
||||
"ORG-SHACK-TECH-DN42",
|
||||
"ORG-YANE-DN42",
|
||||
"SOURIS-DN42",
|
||||
"CCCHB-ABUSE-DN42",
|
||||
"MAGLAB-DN42",
|
||||
"NL-ZUID-DN42",
|
||||
"ORG-SHACK-ADMIN-DN42",
|
||||
"ALENAN-DN42"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
* Returns a list of all objects in types that match 'route'
|
||||
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/registry/*route | jq
|
||||
{
|
||||
"route": [
|
||||
"172.20.28.0_27",
|
||||
"172.23.220.0_24",
|
||||
"172.23.82.0_25",
|
||||
"10.149.0.0_16",
|
||||
|
||||
...
|
||||
|
||||
"172.20.128.0_27",
|
||||
"172.22.127.32_27"
|
||||
],
|
||||
"route-set": [
|
||||
"RS-DN42",
|
||||
"RS-DN42-NATIVE"
|
||||
],
|
||||
"route6": [
|
||||
"fd42:df42::_48",
|
||||
"fd5c:0f0f:39fc::_48",
|
||||
|
||||
...
|
||||
|
||||
"fd16:c638:3d7c::_48",
|
||||
"fd23::_48"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
* Returns the mntner/BURBLE-MNT object (in decorated format)
|
||||
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/registry/mntner/BURBLE-MNT | jq
|
||||
{
|
||||
"mntner/BURBLE-MNT": {
|
||||
"Attributes": [
|
||||
[
|
||||
"mntner",
|
||||
"BURBLE-MNT"
|
||||
],
|
||||
[
|
||||
"descr",
|
||||
"burble.dn42 https://dn42.burble.com/"
|
||||
],
|
||||
[
|
||||
"admin-c",
|
||||
"[BURBLE-DN42](person/BURBLE-DN42)"
|
||||
],
|
||||
[
|
||||
"tech-c",
|
||||
"[BURBLE-DN42](person/BURBLE-DN42)"
|
||||
],
|
||||
[
|
||||
"auth",
|
||||
"pgp-fingerprint 1C08F282095CCDA432AECC657B9FE8780CFB6593"
|
||||
],
|
||||
[
|
||||
"mnt-by",
|
||||
"[BURBLE-MNT](mntner/BURBLE-MNT)"
|
||||
],
|
||||
[
|
||||
"source",
|
||||
"[DN42](registry/DN42)"
|
||||
]
|
||||
],
|
||||
"Backlinks": [
|
||||
"aut-num/AS4242422602",
|
||||
"aut-num/AS4242422601",
|
||||
"mntner/BURBLE-MNT",
|
||||
"route/172.20.129.160_27",
|
||||
"as-set/AS4242422601:AS-DOWNSTREAM",
|
||||
"as-set/AS4242422601:AS-TRANSIT",
|
||||
"person/BURBLE-DN42",
|
||||
"inet6num/fd42:4242:2601::_48",
|
||||
"domain/burble.dn42",
|
||||
"domain/collector.dn42",
|
||||
"route6/fd42:4242:2601::_48",
|
||||
"inetnum/172.20.129.160_27"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Returns error 404, exact searches are case sensitive
|
||||
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/registry/mntner/burble-mnt | jq
|
||||
```
|
||||
|
||||
* Returns domain names matching 'burble' in raw format
|
||||
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/registry/domain/*burble?raw | jq
|
||||
{
|
||||
"domain/burble.dn42": [
|
||||
[
|
||||
"domain",
|
||||
"burble.dn42"
|
||||
],
|
||||
[
|
||||
"descr",
|
||||
"burble.dn42 https://dn42.burble.com/"
|
||||
],
|
||||
[
|
||||
"admin-c",
|
||||
"BURBLE-DN42"
|
||||
],
|
||||
[
|
||||
"tech-c",
|
||||
"BURBLE-DN42"
|
||||
],
|
||||
[
|
||||
"mnt-by",
|
||||
"BURBLE-MNT"
|
||||
],
|
||||
[
|
||||
"nserver",
|
||||
"ns1.burble.dn42 172.20.129.161"
|
||||
],
|
||||
[
|
||||
"nserver",
|
||||
"ns1.burble.dn42 fd42:4242:2601:ac53::1"
|
||||
],
|
||||
[
|
||||
"ds-rdata",
|
||||
"61857 13 2 bd35e3efe3325d2029fb652e01604a48b677cc2f44226eeabee54b456c67680c"
|
||||
],
|
||||
[
|
||||
"source",
|
||||
"DN42"
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
* Returns all objects matching 172.20.0
|
||||
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/registry/*/*172.20.0 | jq
|
||||
{
|
||||
"inetnum/172.20.0.0_14": {
|
||||
"Attributes": [
|
||||
[
|
||||
"inetnum",
|
||||
"172.20.0.0 - 172.23.255.255"
|
||||
],
|
||||
[
|
||||
"cidr",
|
||||
"172.20.0.0/14"
|
||||
],
|
||||
|
||||
... and so on
|
||||
```
|
||||
|
||||
* Returns the nic-hdl attribute for all person objects
|
||||
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/registry/person/*/nic-hdl | jq
|
||||
{
|
||||
"person/0RIGO-DN42": {
|
||||
"nic-hdl": [
|
||||
"0RIGO-DN42"
|
||||
]
|
||||
},
|
||||
"person/0XDRAGON-DN42": {
|
||||
"nic-hdl": [
|
||||
"0XDRAGON-DN42"
|
||||
]
|
||||
},
|
||||
"person/1714-DN42": {
|
||||
"nic-hdl": [
|
||||
"1714-DN42"
|
||||
]
|
||||
},
|
||||
|
||||
... and so on
|
||||
```
|
||||
|
||||
* return raw contact (-c) attributes in aut-num objects that contain 'burble'
|
||||
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/registry/aut-num/*/*-c/*burble?raw | jq
|
||||
{
|
||||
"aut-num/AS4242422601": {
|
||||
"admin-c": [
|
||||
"BURBLE-DN42"
|
||||
],
|
||||
"tech-c": [
|
||||
"BURBLE-DN42"
|
||||
]
|
||||
},
|
||||
"aut-num/AS4242422602": {
|
||||
"admin-c": [
|
||||
"BURBLE-DN42"
|
||||
],
|
||||
"tech-c": [
|
||||
"BURBLE-DN42"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A special query exists to return metadata about the registry
|
||||
|
||||
```
|
||||
GET /api/registry/.meta
|
||||
```
|
||||
|
||||
Example Output (JSON format):
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/dns/.meta | jq
|
||||
```
|
||||
|
||||
```
|
||||
{
|
||||
"Commit": "fa89d022d0c2a48bcfbee405e2f3685f3b9cf063"
|
||||
}
|
||||
```
|
||||
|
||||
## Route Origin Authorisation (ROA) API
|
||||
|
||||
Route Origin Authorisation (ROA) data can be obtained from the server in
|
||||
JSON and bird formats.
|
||||
|
||||
### JSON format output
|
||||
|
||||
```
|
||||
GET /api/roa/json
|
||||
```
|
||||
|
||||
Provides IPv4 and IPv6 ROAs in JSON format, suitable for use with
|
||||
[gortr](https://github.com/cloudflare/gortr).
|
||||
|
||||
Example Output:
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/roa/json | jq
|
||||
```
|
||||
|
||||
```
|
||||
{
|
||||
"metadata": {
|
||||
"counts": 1564,
|
||||
"generated": 1550402199,
|
||||
"valid": 1550445399
|
||||
},
|
||||
"roas": [
|
||||
{
|
||||
"prefix": "172.23.128.0/26",
|
||||
"maxLength": 29,
|
||||
"asn": "AS4242422747"
|
||||
},
|
||||
{
|
||||
"prefix": "172.22.129.192/26",
|
||||
"maxLength": 29,
|
||||
"asn": "AS4242423976"
|
||||
},
|
||||
{
|
||||
"prefix": "10.110.0.0/16",
|
||||
"maxLength": 24,
|
||||
"asn": "AS65110"
|
||||
},
|
||||
|
||||
... and so on
|
||||
```
|
||||
|
||||
### Bird format output
|
||||
|
||||
```
|
||||
GET /api/roa/bird/{bird version}/{IP family}
|
||||
```
|
||||
|
||||
Provides ROA data suitable for including in to bird.
|
||||
|
||||
{bird version} must be either 1 or 2
|
||||
|
||||
{IP family} can be 4, 6 or 46 to provide both IPv4 and IPv6 results
|
||||
|
||||
|
||||
Example Output:
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/roa/bird/1/4
|
||||
```
|
||||
|
||||
```
|
||||
#
|
||||
# dn42regsrv ROA Generator
|
||||
# Last Updated: 2019-02-17 11:16:39.668799525 +0000 GMT m=+0.279049704
|
||||
# Commit: 3cbc349bf770493c016888ff785227ded2a7d866
|
||||
#
|
||||
roa 172.23.128.0/26 max 29 as 4242422747;
|
||||
roa 172.22.129.192/26 max 29 as 4242423976;
|
||||
roa 10.110.0.0/16 max 24 as 65110;
|
||||
roa 172.20.164.0/26 max 29 as 4242423023;
|
||||
roa 172.20.135.200/29 max 29 as 4242420448;
|
||||
roa 10.65.0.0/20 max 24 as 4242420420;
|
||||
roa 172.20.149.136/29 max 29 as 4242420234;
|
||||
roa 10.160.0.0/13 max 24 as 65079;
|
||||
roa 10.169.0.0/16 max 24 as 65534;
|
||||
|
||||
... and so on
|
||||
```
|
||||
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/roa/bird/2/6
|
||||
```
|
||||
|
||||
```
|
||||
#
|
||||
# dn42regsrv ROA Generator
|
||||
# Last Updated: 2019-02-17 11:16:39.668799525 +0000 GMT m=+0.279049704
|
||||
# Commit: 3cbc349bf770493c016888ff785227ded2a7d866
|
||||
#
|
||||
route fdc3:10cd:ae9d::/48 max 64 as 4242420789;
|
||||
route fd41:9805:7b69:4000::/51 max 64 as 4242420846;
|
||||
route fd41:9805:7b69:4000::/51 max 64 as 4242420845;
|
||||
route fd41:9805:7b69:4000::/51 max 64 as 4242420847;
|
||||
route fddf:ebfd:a801:2331::/64 max 64 as 65530;
|
||||
route fd42:1a2b:de57::/48 max 64 as 4242422454;
|
||||
route fd42:7879:7879::/48 max 64 as 4242421787;
|
||||
|
||||
... and so on
|
||||
```
|
||||
|
||||
### filter{,6}.txt
|
||||
|
||||
```
|
||||
GET /api/roa/filter/{IP family}
|
||||
```
|
||||
|
||||
Provides the contents of filter.txt and filter6.txt in json format.
|
||||
|
||||
{IP family} can be 4, 6 or 46 to provide both IPv4 and IPv6 results
|
||||
|
||||
|
||||
Example Output:
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/roa/filter/6 | jq
|
||||
```
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"nr": 1001,
|
||||
"action": "permit",
|
||||
"prefix": "fd00::/8",
|
||||
"minlen": 44,
|
||||
"maxlen": 64
|
||||
},
|
||||
{
|
||||
"nr": 9999,
|
||||
"action": "deny",
|
||||
"prefix": "::/0",
|
||||
"minlen": 0,
|
||||
"maxlen": 128
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## DNS Root Zone API
|
||||
|
||||
The DNS API provides a list of resource records that can be used to create a root zone for DN42
|
||||
related domains. By polling the API, DNS servers are able to keep their root zone delegations and
|
||||
DNSSEC records up to date.
|
||||
|
||||
|
||||
```
|
||||
GET /api/dns/root-zone?format={[json|bind]}
|
||||
```
|
||||
|
||||
Format may either 'json' or 'bind' to provide resource records in either format. The default
|
||||
output format is JSON.
|
||||
|
||||
Example Output (JSON format):
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/dns/root-zone?format=json | jq
|
||||
```
|
||||
|
||||
```
|
||||
{
|
||||
"Records": [
|
||||
{
|
||||
"Name": "dn42",
|
||||
"Type": "NS",
|
||||
"Content": "b.delegation-servers.dn42.",
|
||||
"Comment": "DN42 Authoritative Zone"
|
||||
},
|
||||
{
|
||||
"Name": "dn42",
|
||||
"Type": "NS",
|
||||
"Content": "j.delegation-servers.dn42.",
|
||||
"Comment": "DN42 Authoritative Zone"
|
||||
},
|
||||
|
||||
... and so on
|
||||
```
|
||||
|
||||
Example Output (BIND format):
|
||||
```
|
||||
wget -O - -q http://localhost:8042/api/dns/root-zone?format=bind
|
||||
```
|
||||
|
||||
```
|
||||
;; DN42 Root Zone Records
|
||||
;; Commit Reference: 2cc95d9101268ce82239dee1f947e4a8273524a9
|
||||
;; Generated: 2019-03-08 19:40:51.264803795 +0000 GMT m=+0.197704585
|
||||
dn42 IN NS b.delegation-servers.dn42. ; DN42 Authoritative Zone
|
||||
dn42 IN NS j.delegation-servers.dn42. ; DN42 Authoritative Zone
|
||||
dn42 IN NS y.delegation-servers.dn42. ; DN42 Authoritative Zone
|
||||
dn42 IN DS 64441 10 2 6dadda00f5986bd26fe4f162669742cf7eba07d212b525acac9840ee06cb2799 ; DN42 Authoritative Zone
|
||||
dn42 IN DS 56676 10 2 4b559c949eb796f5502f05bd5bb2143672e7ef935286db552955f291bb81093e ; DN42 Authoritative Zone
|
||||
d.f.ip6.arpa IN NS b.delegation-servers.dn42. ; DN42 Authoritative Zone
|
||||
d.f.ip6.arpa IN NS j.delegation-servers.dn42. ; DN42 Authoritative Zone
|
||||
d.f.ip6.arpa IN NS y.delegation-servers.dn42. ; DN42 Authoritative Zone
|
||||
d.f.ip6.arpa IN DS 64441 10 2 9057500a3b6e09bf45a60ed8891f2e649c6812d5d149c45a3c560fa0a619
|
||||
5c49 ; DN42 Authoritative Zone
|
||||
d.f.ip6.arpa IN DS 56676 10 2 d93cfd941025aaa445283d33e27157bb9a2df0a9c1389fdf5e36a377fc31
|
||||
4736 ; DN42 Authoritative Zone
|
||||
|
||||
... and so on
|
||||
```
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Simon Marsh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
90
README.md
90
README.md
@ -1,3 +1,89 @@
|
||||
# pipeline-test
|
||||
# dn42regsrv
|
||||
|
||||
A REST API for the DN42 registry, written in Go, to provide a bridge between
|
||||
interactive applications and registry data.
|
||||
|
||||
A public instance of the API and explorer web app can be accessed via:
|
||||
|
||||
* [https://explorer.burble.com/](https://explorer.burble.com/) (Internet link)
|
||||
* [http://explorer.collector.dn42/](http://explorer.collector.dn42/) (DN42 Link)
|
||||
|
||||
## Features
|
||||
|
||||
* REST API for querying DN42 registry objects
|
||||
* Able to decorate objects with relationship information based on SCHEMA type definitions
|
||||
* Includes a simple webserver for delivering static files which can be used to deliver
|
||||
basic web applications utilising the API (such as the included DN42 Registry Explorer)
|
||||
* Automatic pull from the DN42 git repository to keep the registry up to date
|
||||
* Includes a responsive web app for exploring the registry
|
||||
* API endpoints for ROA data in JSON, and bird formats
|
||||
* API endpoint to support the creation of DNS root zone records
|
||||
|
||||
## Building
|
||||
|
||||
#### Using locally installed go
|
||||
|
||||
Requires [git](https://git-scm.com/) and [go](https://golang.org)
|
||||
```
|
||||
go get -insecure git.dn42.us/burble/dn42regsrv
|
||||
```
|
||||
|
||||
#### Without installing go
|
||||
|
||||
Using container runtime to build with the golang container:
|
||||
```
|
||||
docker run -v ${PWD}:/go/bin golang go get -insecure git.dn42.us/burble/dn42regsrv
|
||||
```
|
||||
|
||||
Or use the *contrib/build.sh* script after cloning the repo.
|
||||
|
||||
## Running
|
||||
|
||||
#### As a service
|
||||
|
||||
Use --help to view configurable options
|
||||
```
|
||||
${GOPATH}/bin/dn42regsrv --help
|
||||
```
|
||||
|
||||
The server requires access to a clone of the DN42 registry and for
|
||||
the git executable to be accessible.
|
||||
If you want to use the auto pull feature then the registry must
|
||||
also be writable by the server.
|
||||
|
||||
```
|
||||
cd ${GOPTH}/src/git.dn42.us/burble/dn42regsrv
|
||||
git clone http://git.dn42.us/dn42/registry.git
|
||||
${GOPATH}/dn42regsrv
|
||||
```
|
||||
|
||||
A sample service file is included for running the server under systemd
|
||||
|
||||
#### Within a container
|
||||
|
||||
A container build script (*contrib/buildah.sh*) is included in the
|
||||
contrib directory. The script uses [buildah](https://buildah.io/).
|
||||
|
||||
See the *contrib/entrypoint.sh* script for environment variables that can
|
||||
be set when running the container.
|
||||
|
||||
## Using
|
||||
|
||||
By default the server will be listening on port 8042.
|
||||
See the [API.md](API.md) file for a detailed description of the API.
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
Please feel free to raise issues or create pull requests for the project git repository.
|
||||
|
||||
## #ToDo
|
||||
|
||||
### Server
|
||||
|
||||
- Add WHOIS interface
|
||||
|
||||
### DN42 Registry Explorer Web App
|
||||
|
||||
- Allow for attribute searches
|
||||
|
||||
Test pipeline
|
1
StaticRoot/anchorme.min.js
vendored
Normal file
1
StaticRoot/anchorme.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
12
StaticRoot/bootstrap.min.css
vendored
Normal file
12
StaticRoot/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
StaticRoot/dn42_logo.png
Normal file
BIN
StaticRoot/dn42_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
352
StaticRoot/explorer.js
Normal file
352
StaticRoot/explorer.js
Normal file
@ -0,0 +1,352 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry Explorer
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// global store for data that is loaded once
|
||||
const GlobalStore = {
|
||||
data: {
|
||||
RegStats: null,
|
||||
Index: null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// registry object component
|
||||
|
||||
Vue.component('reg-object', {
|
||||
template: '#reg-object-template',
|
||||
props: [ 'link', 'content' ],
|
||||
data() {
|
||||
return { }
|
||||
},
|
||||
methods: {
|
||||
updateSearch: function(str) {
|
||||
vm.updateSearch(str)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rtype: function() {
|
||||
var ix = this.link.indexOf("/")
|
||||
return this.link.substring(0, ix)
|
||||
},
|
||||
obj: function() {
|
||||
var ix = this.link.indexOf("/")
|
||||
return this.link.substring(ix + 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// reg-attribute component
|
||||
|
||||
Vue.component('reg-attribute', {
|
||||
template: '#reg-attribute-template',
|
||||
props: [ 'content' ],
|
||||
data() {
|
||||
return { }
|
||||
},
|
||||
methods: {
|
||||
isRegObject: function(str) {
|
||||
return (this.content.match(/^\[.*?\]\(.*?\)/) != null)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
objectLink: function() {
|
||||
reg = this.content.match(/^\[(.*?)\]\((.*?)\)/)
|
||||
return reg[2]
|
||||
},
|
||||
decorated: function() {
|
||||
var c = this.content
|
||||
|
||||
// an attribute terminated with \n indicates a blank
|
||||
// trailing line, however a single trailing <br/> will
|
||||
// not be rendered in HTML, this hack doubles up the
|
||||
// trailing newline so it creates a <br/> pair
|
||||
// which renders as the blank line
|
||||
if (c.substr(c.length-1) == "\n") {
|
||||
c = c + "\n"
|
||||
}
|
||||
|
||||
// replace newlines with line breaks
|
||||
c = c.replace(/\n/g, "<br/>")
|
||||
|
||||
// decorate
|
||||
c = anchorme(c, {
|
||||
truncate: 40,
|
||||
ips: false,
|
||||
attributes: [ { name: "target", value: "_blank" } ]
|
||||
})
|
||||
|
||||
// and return the final decorated content
|
||||
return c
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// search input component
|
||||
|
||||
Vue.component('search-input', {
|
||||
template: '#search-input-template',
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
searchTimeout: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
debounceSearch: function(value) {
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout)
|
||||
}
|
||||
|
||||
// link should be an absolute path
|
||||
value = '/' + value
|
||||
|
||||
this.searchTimeout = setTimeout(
|
||||
this.$router.push.bind(this.$router, value), 500
|
||||
)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
// listen to search updates and set the search text appropriately
|
||||
this.$root.$on('SearchChanged', value => {
|
||||
this.search = value
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// registry-stats component
|
||||
|
||||
Vue.component('registry-stats', {
|
||||
template: '#registry-stats-template',
|
||||
data() {
|
||||
return {
|
||||
state: null,
|
||||
error: null,
|
||||
store: GlobalStore.data
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
// just fetch the stats from the API server via axios
|
||||
reload(event) {
|
||||
this.store.RegStats = null
|
||||
this.state = "loading"
|
||||
|
||||
axios
|
||||
.get('/api/registry/')
|
||||
.then(response => {
|
||||
this.store.RegStats = response.data
|
||||
this.state = 'complete'
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error
|
||||
this.state = 'error'
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.store.RegStats == null) {
|
||||
this.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// root component
|
||||
|
||||
Vue.component('app-root', {
|
||||
template: '#app-root-template',
|
||||
mounted() {
|
||||
this.$root.$emit('SearchChanged', '')
|
||||
}
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// search results
|
||||
|
||||
Vue.component('app-search', {
|
||||
template: '#app-search-template',
|
||||
data() {
|
||||
return {
|
||||
state: null,
|
||||
search: null,
|
||||
results: null,
|
||||
store: GlobalStore.data
|
||||
}
|
||||
},
|
||||
|
||||
// trigger a search on route update and on mount
|
||||
beforeRouteUpdate(to, from, next) {
|
||||
this.search = to.params.pathMatch
|
||||
this.doSearch()
|
||||
next()
|
||||
},
|
||||
mounted() {
|
||||
// store the search for later
|
||||
this.search = this.$route.params.pathMatch
|
||||
|
||||
// the index must be loaded before any real work takes place
|
||||
if (this.store.Index == null) {
|
||||
this.loadIndex()
|
||||
}
|
||||
else {
|
||||
// index was already loaded, go search
|
||||
this.doSearch()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
// load the search index from the API
|
||||
loadIndex() {
|
||||
|
||||
this.state = 'loading'
|
||||
this.$root.$emit('SearchChanged', 'Initialising ...')
|
||||
|
||||
axios
|
||||
.get('/api/registry/*')
|
||||
.then(response => {
|
||||
this.store.Index = response.data
|
||||
|
||||
// if a query parameter has been passed,
|
||||
// then go search
|
||||
if (this.search != null) {
|
||||
this.$nextTick(this.doSearch())
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.state = 'error'
|
||||
console.log(error)
|
||||
})
|
||||
},
|
||||
|
||||
// substring match object names against a search
|
||||
matchObjects(objects, rtype, term) {
|
||||
var results = [ ]
|
||||
|
||||
// check each object
|
||||
for (const obj in objects) {
|
||||
|
||||
// matches are all lower case
|
||||
var s = objects[obj].toLowerCase()
|
||||
|
||||
var pos = s.indexOf(term)
|
||||
if (pos != -1) {
|
||||
if ((pos == 0) && (s == term)) {
|
||||
// exact match, return just this result
|
||||
return [[ rtype, objects[obj] ]]
|
||||
}
|
||||
results.push([ rtype, objects[obj] ])
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
},
|
||||
|
||||
|
||||
// filter the index to find matches
|
||||
searchFilter() {
|
||||
|
||||
var results = [ ]
|
||||
var index = this.store.Index
|
||||
|
||||
// comparisons are lowercase
|
||||
var term = this.search.toLowerCase()
|
||||
|
||||
// check if search includes a '/'
|
||||
var slash = term.indexOf('/')
|
||||
if (slash != -1) {
|
||||
// match only on the specific type
|
||||
var rtype = term.substring(0, slash)
|
||||
var term = term.substring(slash + 1)
|
||||
objects = index[rtype]
|
||||
|
||||
if (objects != null) {
|
||||
results = this.matchObjects(objects, rtype, term)
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
// walk though the entire index
|
||||
for (const rtype in index) {
|
||||
var objlist = this.matchObjects(index[rtype], rtype, term)
|
||||
results = results.concat(objlist)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
},
|
||||
|
||||
// perform the search and present results
|
||||
doSearch() {
|
||||
// notify other components that the search is updated
|
||||
this.$root.$emit('SearchChanged', this.search)
|
||||
|
||||
// filter matches against the index
|
||||
filtered = this.searchFilter()
|
||||
|
||||
// got nothing ?
|
||||
if (filtered.length == 0) {
|
||||
this.state = "noresults"
|
||||
}
|
||||
|
||||
// just one result
|
||||
else if (filtered.length == 1) {
|
||||
var objname = filtered[0]
|
||||
|
||||
// load the object from the API
|
||||
this.state = 'loading'
|
||||
query = '/api/registry/' + objname[0] + '/' + objname[1]
|
||||
|
||||
axios
|
||||
.get(query)
|
||||
.then(response => {
|
||||
this.state = 'result'
|
||||
this.result = response.data
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error
|
||||
this.state = 'error'
|
||||
})
|
||||
}
|
||||
|
||||
// lots of results
|
||||
else {
|
||||
this.state = "resultlist"
|
||||
this.result = filtered
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// main vue application starts here
|
||||
|
||||
// initialise the Vue Router
|
||||
const router = new VueRouter({
|
||||
routes: [
|
||||
{ path: '/', component: Vue.component('app-root') },
|
||||
{ path: '/*', component: Vue.component('app-search') }
|
||||
]
|
||||
})
|
||||
|
||||
// and the main app instance
|
||||
const vm = new Vue({
|
||||
el: '#explorer_app',
|
||||
data: {
|
||||
|
||||
},
|
||||
router
|
||||
})
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
218
StaticRoot/index.html
Normal file
218
StaticRoot/index.html
Normal file
@ -0,0 +1,218 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>DN42 Registry Explorer</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<!-- Style overrides -->
|
||||
<style>
|
||||
.material-icons { display:inline-flex;vertical-align:middle }
|
||||
body { box-shadow: inset 0 2em 10em rgba(0,0,0,0.4); min-height: 100vh }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="explorer_app">
|
||||
|
||||
<nav class="navbar navbar-fixed-top navbar-expand-md navbar-dark bg-dark">
|
||||
<search-input></search-input>
|
||||
<div class="collapse navbar-collapse w-100 ml-auto text-nowrap">
|
||||
<div class="ml-auto"><router-link class="navbar-brand"
|
||||
to="/">Registry Explorer</router-link> <a class="pull-right navbar-brand"
|
||||
href="https://dn42.us/"><img src="/dn42_logo.png" width="173" height="60"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<router-view></router-view>
|
||||
|
||||
</div>
|
||||
<footer class="page-footer font-small">
|
||||
<div style="margin-top: 20px; padding: 5px">
|
||||
<a href="https://git.dn42.us/burble/dn42regsrv">Source Code</a>.
|
||||
Powered by
|
||||
<a href="https://getbootstrap.com/">Bootstrap</a>,
|
||||
<a href="https://vuejs.org">Vue.js</a>,
|
||||
<a href="https://github.com/axios/axios">axios</a>,
|
||||
<a href="http://alexcorvi.github.io/anchorme.js/">Anchorme</a>.
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="text/x-template" id="search-input-template">
|
||||
<form class="form-inline" id="SearchForm">
|
||||
<input v-bind:value="search"
|
||||
v-on:input="debounceSearch($event.target.value)"
|
||||
class="form-control-lg" size="30" type="search"
|
||||
placeholder="Search the registry" aria-label="Search"/>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="app-root-template">
|
||||
<div class="p-3">
|
||||
<div class="jumbotron">
|
||||
<h1>DN42 Registry Explorer</h1>
|
||||
<p class="lead">Just start typing in the search box to start searching the registry</p>
|
||||
<hr/>
|
||||
<p>
|
||||
<p>Search Tips</p>
|
||||
<ul>
|
||||
<li>Searches are case independent
|
||||
<li>No need to hit enter, searches will start immediately
|
||||
<li>Prefixing the search by a registry type followed by / will narrow the search to
|
||||
just that type (e.g. <router-link class="text-success"
|
||||
to="domain/.dn42">domain/.dn42</router-link>)</li>
|
||||
<li>Searching for <b>type/</b> will return all the objects for that type (e.g.
|
||||
<router-link class="text-success" to="schema/">schema/</router-link>)</li>
|
||||
<li>A blank search box will return you to these instructions</li>
|
||||
<li>Just copy the URL to link to search results</li>
|
||||
<li>Searches are made on object names; searching the content of objects
|
||||
is not supported (yet!).</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<p>
|
||||
The registry explorer is a simple web app using
|
||||
<a href="https://git.dn42.us/burble/dn42regsrv">dn42regsrv</a>;
|
||||
a REST API for the DN42 registry built with <a href="https://golang.org/">Go</a>
|
||||
<br/>
|
||||
The DN42 registry contains personal data which is also available through this site.
|
||||
Please refer to the DN42 registry privacy policy, or contact dn42@burble.com for any
|
||||
queries.
|
||||
</p>
|
||||
</div>
|
||||
<registry-stats></registry-stats>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="registry-stats-template">
|
||||
<div class="container d-flex flex-column w-75">
|
||||
<h5>Registry Stats</h5>
|
||||
<section v-if="state == 'error'">
|
||||
<div class="alert alert-warning clearfix" role="alert">
|
||||
An error recurred whilst retrieving data from the API
|
||||
<button type="button" class="float-right btn btn-primary"
|
||||
v-on:click="reload"><span class="material-icons">refresh</span>
|
||||
Refresh</button>
|
||||
</div>
|
||||
</section>
|
||||
<section v-else-if="state == 'loading'">
|
||||
<div class="alert alert-info" role="alert">Loading data ...</div>
|
||||
</section>
|
||||
<section v-else>
|
||||
<p class="p-3 text-center">
|
||||
<span v-for="(value, key) in store.RegStats"
|
||||
class="mx-2 text-nowrap d-inline-block">
|
||||
<router-link v-bind:to="'/'+key+'/'">{{ key }}</router-link>
|
||||
: <b>{{ value }}</b>
|
||||
</span>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="app-search-template">
|
||||
<div class="p-3">
|
||||
|
||||
<section v-if="state == 'loading'">
|
||||
<div class="alert alert-info" role="alert">Loading data ...</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="state == 'error'">
|
||||
<div class="alert alert-warning clearfix" role="alert">
|
||||
An error recurred whilst retrieving search results
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="state == 'noresults'">
|
||||
<h2>Searching for "{{ search }}" ...</h2>
|
||||
<div class="alert alert-dark" role="alert">
|
||||
Sorry, no results found
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="state == 'resultlist'">
|
||||
<h2>Listing results for "{{ search }}" ...</h2>
|
||||
<div class="container d-flex flex-row flex-wrap">
|
||||
<div class="text-center">
|
||||
<span v-for="ref in result">
|
||||
<reg-object class="mx-4 my-2" v-bind:link="ref[0]+'/'+ref[1]"></reg-object>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="state == 'result'">
|
||||
<div v-for="(val, key) in result">
|
||||
<h2 class="mb-4"><reg-object v-bind:link="key"></reg-object></h2>
|
||||
<div class="pl-4">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th scope="col">Key</th><th scope="col">Value</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="a in val.Attributes">
|
||||
<th scope="row" class="border-0 py-1 pr-3 text-primary text-nowrap">
|
||||
{{ a[0] }}
|
||||
</th>
|
||||
<td class="border-0 p-1">
|
||||
<reg-attribute v-bind:content="a[1]"></reg-attribute>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<section v-if="val.Backlinks.length != 0">
|
||||
<p>Referenced by</p>
|
||||
<div class="pl-4">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr v-for="rlink in val.Backlinks">
|
||||
<td class="border-0 p-1">
|
||||
<reg-object v-bind:link="rlink"></reg-object>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Something went wrong, an invalid search state was encountered ({{ state }})
|
||||
<p>{{ $route.params.pathMatch }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="reg-object-template">
|
||||
<span class="text-nowrap d-inline-block">
|
||||
<router-link class="text-success mr-2" v-bind:to="'/'+link">
|
||||
{{ ( content ? content : obj ) }}
|
||||
</router-link>
|
||||
<span class="badge back-pill badge-dark test-muted">{{ rtype }}</span>
|
||||
</span>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="reg-attribute-template">
|
||||
<span style="word-break: break-all; white-space: pre-wrap">
|
||||
<reg-object v-if="isRegObject()" v-bind:link="objectLink"></reg-object>
|
||||
<span v-else class="text-monospace" v-html="decorated"></span>
|
||||
</span>
|
||||
</script>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/vue"></script -->
|
||||
<script src="https://unpkg.com/vue-router"></script -->
|
||||
<script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script>
|
||||
<!-- script src="https://unpkg.com/vue@2.6.2/dist/vue.min.js"></script -->
|
||||
<!-- script src="https://unpkg.com/vue-router@3.0.2/dist/vue-router.js"></script -->
|
||||
<script src="anchorme.min.js"></script>
|
||||
<script src="explorer.js"></script>
|
||||
</body>
|
||||
</html>
|
26
contrib/build.sh
Executable file
26
contrib/build.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
##########################################################################
|
||||
# A small script to build a static dn42regsrv image
|
||||
# using the golang container image
|
||||
#
|
||||
# the binary will be built in to the current directory
|
||||
##########################################################################
|
||||
RUNTIME=$(which podman || which docker)
|
||||
echo "Using container runtime: ${RUNTIME}"
|
||||
|
||||
# find the source directory
|
||||
SCRIPTPATH="$(cd "$(dirname "$0")" ; pwd -P)"
|
||||
SOURCEPATH="$(cd "${SCRIPTPATH}/../"; pwd -P)"
|
||||
echo "Source is in: ${SOURCEPATH}"
|
||||
|
||||
# do the thing
|
||||
${RUNTIME} run --rm \
|
||||
-e CGO_ENABLED=0 \
|
||||
-v "${SOURCEPATH}:/go/src/dn42regsrv" \
|
||||
-v "${PWD}:/go/bin" \
|
||||
-w "/go/src/dn42regsrv" \
|
||||
docker.io/golang:1.12 \
|
||||
go get
|
||||
|
||||
##########################################################################
|
||||
# end of code
|
65
contrib/buildah.sh
Executable file
65
contrib/buildah.sh
Executable file
@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
##########################################################################
|
||||
echo "Building dn42regsrv container"
|
||||
|
||||
# find the source directory
|
||||
SCRIPTPATH="$(cd "$(dirname "$0")" ; pwd -P)"
|
||||
SOURCEPATH="$(cd "${SCRIPTPATH}/../"; pwd -P)"
|
||||
echo "Source is in: ${SOURCEPATH}"
|
||||
|
||||
##########################################################################
|
||||
|
||||
DEPS='git'
|
||||
B=$(which buildah)
|
||||
|
||||
# initialise container
|
||||
c=$(buildah from --name dn42regsrv-working docker.io/debian:buster)
|
||||
|
||||
##########################################################################
|
||||
|
||||
# install dependencies and initialise directories
|
||||
$B run $c -- bash <<EOF
|
||||
apt-get -y update
|
||||
apt-get -y install --no-install-recommends $DEPS
|
||||
rm -r /var/lib/apt/lists
|
||||
EOF
|
||||
|
||||
# mount the container
|
||||
m=$($B mount $c)
|
||||
|
||||
# create directories and copy the web app
|
||||
mkdir "$m/app" "$m/data" "$m/registry" "$m/data/ssh"
|
||||
|
||||
# web app
|
||||
cp -r "$SOURCEPATH/StaticRoot" "$m/data/webapp"
|
||||
|
||||
# add the entrypoint.sh script
|
||||
cp "$SOURCEPATH/contrib/entrypoint.sh" "$m/app"
|
||||
chmod +x "$m/app"
|
||||
|
||||
# build the binary directly in to the container
|
||||
pushd "$m/app"
|
||||
"$SOURCEPATH/contrib/build.sh"
|
||||
popd
|
||||
|
||||
# unmount the container
|
||||
$B unmount $c
|
||||
|
||||
# configure
|
||||
buildah config \
|
||||
--author="Simon Marsh" \
|
||||
--workingdir='/data/registry' \
|
||||
--cmd='/app/entrypoint.sh' \
|
||||
$c
|
||||
|
||||
##########################################################################
|
||||
# finally create the image
|
||||
|
||||
echo "Committing image ..."
|
||||
i=$($B commit --squash $c dn42regsrv)
|
||||
|
||||
# clean up
|
||||
$B rm $c
|
||||
|
||||
##########################################################################
|
||||
# end of file
|
33
contrib/dn42regsrv.service
Normal file
33
contrib/dn42regsrv.service
Normal file
@ -0,0 +1,33 @@
|
||||
##########################################################################
|
||||
# dn42regsrv example systemd service file
|
||||
##########################################################################
|
||||
|
||||
[Unit]
|
||||
Description=DN42 Registry API Server
|
||||
After=network.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
User=regsrv
|
||||
Group=registry
|
||||
Type=simple
|
||||
Restart=on-failure
|
||||
# service hardening
|
||||
ProtectSystem=strict
|
||||
ReadOnlyPaths=/home/regsrv/go/src/git.dn42.us/burble/dn42regsrv/StaticRoot
|
||||
ReadWritePaths=/home/regsrv/registry
|
||||
NoNewPrivileges=yes
|
||||
ProtectControlGroups=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
DevicePolicy=closed
|
||||
MemoryDenyWriteExecute=yes
|
||||
#
|
||||
ExecStart=/home/regsrv/go/bin/dn42regsrv \
|
||||
-s /home/regsrv/go/src/git.dn42.us/burble/dn42regsrv/StaticRoot \
|
||||
-d /home/regsrv/registry
|
||||
|
||||
#########################################################################
|
||||
# end of file
|
5
contrib/docker/README.md
Normal file
5
contrib/docker/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# How to run
|
||||
|
||||
* Run ./build.sh
|
||||
* Rename _env to .env and replace REGISTRYDIR with the path to your dn42 registry clone
|
||||
* docker-compose up
|
1
contrib/docker/_env
Normal file
1
contrib/docker/_env
Normal file
@ -0,0 +1 @@
|
||||
REGISTRYDIR=/somedir
|
6
contrib/docker/build.sh
Executable file
6
contrib/docker/build.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker run -it -v $(dirname "$(dirname $PWD)"):/go/src/dn42regsrv golang:alpine ash -c 'apk add git && cd src/dn42regsrv && go get && cp /go/bin/dn42regsrv .'
|
||||
cd ../../
|
||||
docker build -t dn42regsrv -f contrib/docker/build/Dockerfile .
|
||||
rm -f dn42regsrv
|
6
contrib/docker/build/Dockerfile
Normal file
6
contrib/docker/build/Dockerfile
Normal file
@ -0,0 +1,6 @@
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
RUN apk add git
|
||||
COPY dn42regsrv /app/
|
||||
COPY StaticRoot /app/StaticRoot
|
||||
ENTRYPOINT ["/app/dn42regsrv"]
|
9
contrib/docker/docker-compose.yml
Normal file
9
contrib/docker/docker-compose.yml
Normal file
@ -0,0 +1,9 @@
|
||||
version: '2'
|
||||
services:
|
||||
dn42regsrv:
|
||||
image: dn42regsrv
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:8042:8042
|
||||
volumes:
|
||||
- ${REGISTRYDIR}:/app/registry
|
22
contrib/entrypoint.sh
Executable file
22
contrib/entrypoint.sh
Executable file
@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
##########################################################################
|
||||
|
||||
DN42REGSRV_WEBAPP=${DN42REGSRV_WEBAPP:-/data/webapp}
|
||||
DN42REGSRV_REGDIR=${DN42REGSRV_REGDIR:-/data/registry}
|
||||
DN42REGSRV_BRANCH=${DN42REGSRV_BRANCH:-master}
|
||||
DN42REGSRV_BIND=${DN42REGSRV_BIND:-[::]:8042}
|
||||
DN42REGSRV_INTERVAL=${DN42REGSRV_INTERVAL:-10m}
|
||||
DN42REGSRV_LOGLVL=${DN42REGSRV_LOGLVL:-info}
|
||||
DN42REGSRV_AUTOPULL=${DN42REGSRV_AUTOPULL:-true}
|
||||
|
||||
exec /app/dn42regsrv \
|
||||
-s "$DN42REGSRV_WEBAPP" \
|
||||
-d "$DN42REGSRV_REGDIR" \
|
||||
-p "$DN42REGSRV_BRANCH" \
|
||||
-b "$DN42REGSRV_BIND" \
|
||||
-i "$DN42REGSRV_INTERVAL" \
|
||||
-l "$DN42REGSRV_LOGLVL" \
|
||||
-a "$DN42REGSRV_AUTOPULL"
|
||||
|
||||
##########################################################################
|
||||
# end of file
|
294
contrib/sync_rootzone.sh
Executable file
294
contrib/sync_rootzone.sh
Executable file
@ -0,0 +1,294 @@
|
||||
#!/bin/bash
|
||||
##########################################################################
|
||||
#
|
||||
# This script is intended to be run via cron job to sync a local,
|
||||
# authoritative DNS server's root zone with the registry information
|
||||
# provided by dn42regsrv.
|
||||
#
|
||||
# As is, the script is specific to updating PDNS within the burble.dn42
|
||||
# network, however it is also intended to be easily adaptable to other
|
||||
# DNS servers and networks
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
##########################################################################
|
||||
# This array is used to define additional, local networks that the
|
||||
# DNS server may be authoritative for. The array is used to prevent
|
||||
# the related resource records from being removed automatically,
|
||||
# as any records not listed here or in the registry will get deleted.
|
||||
# Make sure to include '.' here or the local NS and SOA records
|
||||
# will get removed
|
||||
|
||||
IGNORE_RECORDS=(
|
||||
'.'
|
||||
'$ORIGIN'
|
||||
'ns1.burble.dn42'
|
||||
'burble.dn42'
|
||||
'collector.dn42'
|
||||
'1.0.6.2.2.4.2.4.2.4.d.f.ip6.arpa'
|
||||
'160/27.129.20.172.in-addr.arpa'
|
||||
'0/27.129.20.172.in-addr.arpa'
|
||||
)
|
||||
|
||||
##########################################################################
|
||||
# The functions here are used to actually update the DNS server
|
||||
# Change these to use a different server than PDNS
|
||||
|
||||
PDNSUTIL='/usr/bin/pdnsutil'
|
||||
PDNSCTRL='/usr/bin/pdns_control'
|
||||
|
||||
# replace_rr <name> <type> <content>
|
||||
function replace_rr {
|
||||
local rr_name=$1; shift
|
||||
local rr_type=$1; shift
|
||||
local rr_content=$*
|
||||
|
||||
echo "Replace: ${rr_name} ${rr_type} '${rr_content}'"
|
||||
|
||||
if [ ${DEBUG} -eq 0 ]
|
||||
then
|
||||
${PDNSUTIL} replace-rrset . ${rr_name} ${rr_type} "${rr_content}"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# delete_rr <name> <type>
|
||||
function delete_rr {
|
||||
local rr_name=$1
|
||||
local rr_type=$2
|
||||
|
||||
echo "Delete: ${rr_name} ${rr_type}"
|
||||
|
||||
if [ ${DEBUG} -eq 0 ]
|
||||
then
|
||||
${PDNSUTIL} delete-rrset . ${rr_name} ${rr_type}
|
||||
fi
|
||||
}
|
||||
|
||||
# list the current contents of the root zone
|
||||
function get_current_root_zone {
|
||||
local rra rr_name rr_type rr_content
|
||||
while read -r -a rra
|
||||
do
|
||||
rr_name=${rra[0]}
|
||||
rr_type=${rra[3]}
|
||||
rr_content=${rra[@]:4}
|
||||
|
||||
current_rz["${rr_name} ${rr_type}"]=${rr_content}
|
||||
|
||||
done < <(${PDNSUTIL} list-zone .)
|
||||
}
|
||||
|
||||
# update the . SOA record after a change
|
||||
function update_soa {
|
||||
echo "Incrementing SOA serial"
|
||||
if [ ${DEBUG} -eq 0 ]
|
||||
then
|
||||
${PDNSUTIL} increase-serial .
|
||||
fi
|
||||
}
|
||||
|
||||
# used to trigger a notify to any slaves of this server
|
||||
function notify_slaves {
|
||||
echo "Notfying slaves"
|
||||
if [ ${DEBUG} -eq 0 ]
|
||||
then
|
||||
${PDNSCTRL} notify .
|
||||
fi
|
||||
}
|
||||
|
||||
##########################################################################
|
||||
# No further local customisation should be needed from here
|
||||
##########################################################################
|
||||
|
||||
# initialise script parameters and global vars
|
||||
|
||||
function usage {
|
||||
echo "Usage: $0 [-h] [-d] [-a URL] [-c FILE]"
|
||||
echo " -h: this help"
|
||||
echo " -d: enable debugging and don't action changes"
|
||||
echo " -a: URL to dn42regsrv API"
|
||||
echo " -c: file in which to store previous commit number"
|
||||
}
|
||||
|
||||
# default options
|
||||
DEBUG=0
|
||||
APIURL="http://explorer.burble.dn42/api"
|
||||
COMMITFILE="/tmp/.sync_rz_commit"
|
||||
|
||||
# parse any arguments passed to the script
|
||||
while getopts ":hda:" opt
|
||||
do
|
||||
case ${opt} in
|
||||
d)
|
||||
DEBUG=1
|
||||
;;
|
||||
a)
|
||||
APIURL=${OPTARG}
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# global vars
|
||||
declare -A current_rz
|
||||
declare -A new_rz
|
||||
current_commit=''
|
||||
new_commit=''
|
||||
deleted_records=0
|
||||
updated_records=0
|
||||
|
||||
##########################################################################
|
||||
# fetch and parse the root zone data from the API
|
||||
|
||||
function fetch_new_root_zone {
|
||||
local line fields rr_name rr_type rr_content
|
||||
while read -r line
|
||||
do
|
||||
if [[ ${line} == ';; Commit Reference:'* ]]
|
||||
then
|
||||
new_commit=${line#;; Commit Reference: }
|
||||
else
|
||||
# strip out comments and create array
|
||||
fields=( ${line%%;*} )
|
||||
|
||||
# if the line is a valid record
|
||||
if [ ${#fields[@]} -ge 4 ]
|
||||
then
|
||||
rr_name=${fields[0]}
|
||||
rr_type=${fields[2]}
|
||||
rr_content=${fields[@]:3}
|
||||
new_rz["${rr_name} ${rr_type}"]=${rr_content}
|
||||
fi
|
||||
fi
|
||||
done < <(/usr/bin/wget -O - -q "${APIURL}/dns/root-zone?format=bind")
|
||||
|
||||
if [ ${DEBUG} -eq 1 ]; then echo "New Commit: ${new_commit}"; fi
|
||||
}
|
||||
|
||||
##########################################################################
|
||||
# load and store the previous commit
|
||||
|
||||
function load_current_commit {
|
||||
read -r current_commit < "${COMMITFILE}"
|
||||
if [ ${DEBUG} -eq 1 ]; then echo "Current Commit: ${current_commit}"; fi
|
||||
}
|
||||
|
||||
function store_current_commit {
|
||||
if [ ${DEBUG} -eq 0 ]
|
||||
then
|
||||
echo "${1}" > "${COMMITFILE}"
|
||||
fi
|
||||
}
|
||||
|
||||
##########################################################################
|
||||
# remove records that have been deleted
|
||||
|
||||
function is_ignored {
|
||||
local rr_name=$1
|
||||
for i in "${IGNORE_RECORDS[@]}"
|
||||
do
|
||||
[ "${i}" == "${rr_name}" ] && echo "ignored"
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
function remove_deleted_records {
|
||||
local key rr_name ignored
|
||||
deleted_records=0
|
||||
|
||||
# check each record in the old root zone
|
||||
for key in "${!current_rz[@]}"
|
||||
do
|
||||
ignored=$(is_ignored ${key% })
|
||||
|
||||
# if record is not ignored, and no new record exists
|
||||
if [ "${ignored}" == '' -a "${new_rz[${key}]}" == '' ]
|
||||
then
|
||||
# then get rid of it
|
||||
delete_rr ${key}
|
||||
deleted_records=$((deleted_records + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Deleted ${deleted_records} records"
|
||||
}
|
||||
|
||||
##########################################################################
|
||||
# update records that have been added or changed
|
||||
|
||||
function update_new_records {
|
||||
local key content
|
||||
updated_records=0
|
||||
|
||||
# check each new record
|
||||
for key in "${!new_rz[@]}"
|
||||
do
|
||||
content="${new_rz[${key}]}"
|
||||
|
||||
# if old record didn't exist, or content differs
|
||||
if [ "${current_rz[${key}]}" != "${content}" ]
|
||||
then
|
||||
# update the record
|
||||
replace_rr $key ${content}
|
||||
updated_records=$((updated_records + 1))
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
echo "Updated ${updated_records} records"
|
||||
}
|
||||
|
||||
##########################################################################
|
||||
# main flow of the script starts here
|
||||
|
||||
echo "DN42 Root Zone Sync"
|
||||
date
|
||||
echo
|
||||
|
||||
fetch_new_root_zone
|
||||
|
||||
# check that the commit was populated
|
||||
if [ "${new_commit}" == '' ]
|
||||
then
|
||||
echo "Unable to fetch new root zone, aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
load_current_commit
|
||||
|
||||
# now check if anything actually needs to be done
|
||||
if [ "${new_commit}" == "${current_commit}" ]
|
||||
then
|
||||
echo "Commits are equal, nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
get_current_root_zone
|
||||
|
||||
# apply changes
|
||||
remove_deleted_records
|
||||
update_new_records
|
||||
|
||||
# bail out if there were no actual differences
|
||||
if [ $((deleted_records + updated_records)) -eq 0 ]
|
||||
then
|
||||
echo "No records were updated, exiting"
|
||||
else
|
||||
|
||||
# update the SOA and send out a notification to slaves
|
||||
update_soa
|
||||
notify_slaves
|
||||
|
||||
fi
|
||||
|
||||
# finally store the new commit to show it's been updated
|
||||
store_current_commit "${new_commit}"
|
||||
|
||||
echo "All done"
|
||||
|
||||
##########################################################################
|
||||
# end of code
|
202
dn42regsrv.go
Normal file
202
dn42regsrv.go
Normal file
@ -0,0 +1,202 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry API Server
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
flag "github.com/spf13/pflag"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// simple event bus
|
||||
|
||||
type NotifyFunc func(...interface{})
|
||||
type SimpleEventBus map[string][]NotifyFunc
|
||||
|
||||
var EventBus = make(SimpleEventBus)
|
||||
|
||||
// add a listener to an event
|
||||
func (bus SimpleEventBus) Listen(event string, nfunc NotifyFunc) {
|
||||
bus[event] = append(bus[event], nfunc)
|
||||
}
|
||||
|
||||
// fire notifications for an event
|
||||
func (bus SimpleEventBus) Fire(event string, params ...interface{}) {
|
||||
funcs := bus[event]
|
||||
if funcs != nil {
|
||||
for _, nfunc := range funcs {
|
||||
nfunc(params...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// utility func for returning JSON from an API endpoint
|
||||
|
||||
func ResponseJSON(w http.ResponseWriter, v interface{}) {
|
||||
|
||||
// for response time testing
|
||||
//time.Sleep(time.Second)
|
||||
|
||||
// marshal the JSON string
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Error("Failed to marshal JSON")
|
||||
}
|
||||
|
||||
// write back to http handler
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// utility function to set the log level
|
||||
|
||||
func setLogLevel(levelStr string) {
|
||||
|
||||
if level, err := log.ParseLevel(levelStr); err != nil {
|
||||
// failed to set the level
|
||||
|
||||
// set a sensible default and, of course, log the error
|
||||
log.SetLevel(log.InfoLevel)
|
||||
log.WithFields(log.Fields{
|
||||
"loglevel": levelStr,
|
||||
"error": err,
|
||||
}).Error("Failed to set requested log level")
|
||||
|
||||
} else {
|
||||
|
||||
// set the requested level
|
||||
log.SetLevel(level)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// http request logger
|
||||
|
||||
func requestLogger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"method": r.Method,
|
||||
"URL": r.URL.String(),
|
||||
"Remote": r.RemoteAddr,
|
||||
}).Debug("HTTP Request")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// everything starts here
|
||||
|
||||
func main() {
|
||||
|
||||
// set a default log level, so that logging can be used immediately
|
||||
// the level will be overidden later on once the command line
|
||||
// options are loaded
|
||||
log.SetLevel(log.InfoLevel)
|
||||
log.Info("DN42 Registry API Server Starting")
|
||||
|
||||
// declare cmd line options
|
||||
var (
|
||||
logLevel = flag.StringP("LogLevel", "l", "Info", "Log level")
|
||||
regDir = flag.StringP("RegDir", "d", "registry", "Registry data directory")
|
||||
bindAddress = flag.StringP("BindAddress", "b", "[::]:8042", "Server bind address")
|
||||
staticRoot = flag.StringP("StaticRoot", "s", "StaticRoot", "Static page directory")
|
||||
refreshInterval = flag.StringP("Refresh", "i", "60m", "Refresh interval")
|
||||
gitPath = flag.StringP("GitPath", "g", "/usr/bin/git", "Path to git executable")
|
||||
autoPull = flag.BoolP("AutoPull", "a", true, "Automatically pull the registry")
|
||||
branch = flag.StringP("Branch", "p", "master", "git branch to pull")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
// now initialise logging properly based on the cmd line options
|
||||
setLogLevel(*logLevel)
|
||||
|
||||
// parse the refreshInterval and start data collection
|
||||
interval, err := time.ParseDuration(*refreshInterval)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"interval": *refreshInterval,
|
||||
}).Fatal("Unable to parse registry refresh interval")
|
||||
}
|
||||
|
||||
InitialiseRegistryData(*regDir, interval,
|
||||
*gitPath, *autoPull, *branch)
|
||||
|
||||
// initialise router
|
||||
router := mux.NewRouter()
|
||||
// log all access
|
||||
router.Use(requestLogger)
|
||||
|
||||
// add API routes
|
||||
subr := router.PathPrefix("/api").Subrouter()
|
||||
EventBus.Fire("APIEndpoint", subr)
|
||||
|
||||
// initialise static routes
|
||||
InstallStaticRoutes(router, *staticRoot)
|
||||
|
||||
// initialise http server
|
||||
server := &http.Server{
|
||||
Addr: *bindAddress,
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
// run the server in a non-blocking goroutine
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"BindAddress": *bindAddress,
|
||||
}).Info("Starting server")
|
||||
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"BindAddress": *bindAddress,
|
||||
}).Fatal("Unable to start server")
|
||||
}
|
||||
}()
|
||||
|
||||
// graceful shutdown via SIGINT (^C)
|
||||
csig := make(chan os.Signal, 1)
|
||||
signal.Notify(csig, os.Interrupt)
|
||||
|
||||
// and block
|
||||
<-csig
|
||||
|
||||
log.Info("Server shutting down")
|
||||
|
||||
// deadline for server to shutdown
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10)
|
||||
defer cancel()
|
||||
|
||||
// shutdown the server
|
||||
server.Shutdown(ctx)
|
||||
|
||||
// nothing left to do
|
||||
log.Info("Shutdown complete, all done")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
234
dnsapi.go
Normal file
234
dnsapi.go
Normal file
@ -0,0 +1,234 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry API Server
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
// "math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// register the api
|
||||
|
||||
func init() {
|
||||
EventBus.Listen("APIEndpoint", InitDNSAPI)
|
||||
EventBus.Listen("RegistryUpdate", DNSUpdate)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// data model
|
||||
|
||||
// very simple DNS record data
|
||||
type DNSRecord struct {
|
||||
Name string
|
||||
Type string
|
||||
Content string
|
||||
Comment string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type DNSZone struct {
|
||||
Records []*DNSRecord
|
||||
Commit string
|
||||
Generated time.Time
|
||||
}
|
||||
|
||||
var DNSRootZone *DNSZone
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// fixed set of authoritative zones
|
||||
|
||||
var DNSRootAuthZones = map[string]string{
|
||||
"dn42": "domain/dn42",
|
||||
"recursive-servers.dn42": "domain/recursive-servers.dn42",
|
||||
"delegation-servers.dn42": "domain/delegation-servers.dn42",
|
||||
"d.f.ip6.arpa": "inet6num/fd00::_8",
|
||||
"20.172.in-addr.arpa": "inetnum/172.20.0.0_16",
|
||||
"21.172.in-addr.arpa": "inetnum/172.21.0.0_16",
|
||||
"22.172.in-addr.arpa": "inetnum/172.22.0.0_16",
|
||||
"23.172.in-addr.arpa": "inetnum/172.23.0.0_16",
|
||||
"31.172.in-addr.arpa": "inetnum/172.31.0.0_16",
|
||||
"10.in-addr.arpa": "inetnum/10.0.0.0_8",
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// called from main to initialise the API routing
|
||||
|
||||
func InitDNSAPI(params ...interface{}) {
|
||||
|
||||
router := params[0].(*mux.Router)
|
||||
|
||||
s := router.
|
||||
Methods("GET").
|
||||
PathPrefix("/dns").
|
||||
Subrouter()
|
||||
|
||||
s.HandleFunc("/root-zone", dnsRZoneHandler)
|
||||
|
||||
log.Info("DNS API installed")
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// api handlers
|
||||
|
||||
// return records that should be included in a DN42 root zone
|
||||
func dnsRZoneHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var format []string
|
||||
query := r.URL.Query()
|
||||
format = query["format"]
|
||||
if format == nil || len(format) != 1 {
|
||||
format = []string{"json"}
|
||||
}
|
||||
|
||||
switch format[0] {
|
||||
case "bind":
|
||||
DNSRootZone.WriteBindFormat(w)
|
||||
|
||||
case "json":
|
||||
ResponseJSON(w, DNSRootZone)
|
||||
|
||||
default:
|
||||
ResponseJSON(w, DNSRootZone)
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// called whenever the registry is updated
|
||||
|
||||
func DNSUpdate(params ...interface{}) {
|
||||
|
||||
registry := params[0].(*Registry)
|
||||
// path := params[1].(string)
|
||||
|
||||
zone := &DNSZone{
|
||||
Generated: time.Now(),
|
||||
Commit: registry.Commit,
|
||||
}
|
||||
|
||||
// add zones that are authoritative within DN42
|
||||
for name, object := range DNSRootAuthZones {
|
||||
zone.AddRecords(registry, name, object, "DN42 Authoritative Zone")
|
||||
}
|
||||
|
||||
// search all domain objects and add stub records for each TLD
|
||||
rtype := registry.Types["domain"]
|
||||
for name, object := range rtype.Objects {
|
||||
// domain is a TLD if it doesn't contain a '.'
|
||||
if strings.IndexRune(name, '.') == -1 {
|
||||
// don't include zones which are authoritative within DN42
|
||||
if DNSRootAuthZones[name] == "" {
|
||||
zone.AddRecords(registry, name, object.Ref, "Forward Zone")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DNSRootZone = zone
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// utility function to add a DNS record to a zone
|
||||
|
||||
func (zone *DNSZone) AddRecord(name string, t string,
|
||||
content string, comment string) {
|
||||
record := &DNSRecord{
|
||||
Name: name,
|
||||
Type: t,
|
||||
Content: content,
|
||||
Comment: comment,
|
||||
}
|
||||
zone.Records = append(zone.Records, record)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// add nserver and ds-rdata records from a registry object
|
||||
|
||||
func (zone *DNSZone) AddRecords(registry *Registry, name string,
|
||||
path string, comment string) {
|
||||
|
||||
// use the registry metadata key index to find the appropriate values
|
||||
object := registry.GetObject(path)
|
||||
if object == nil {
|
||||
log.WithFields(log.Fields{
|
||||
"zone": name,
|
||||
"path": path,
|
||||
}).Error("DNS: unable to find object in registry")
|
||||
return
|
||||
}
|
||||
|
||||
nserver := object.GetKey("nserver")
|
||||
for _, ns := range nserver {
|
||||
// check if stub record needs to be added
|
||||
fields := strings.Split(ns.RawValue, " ")
|
||||
if len(fields) == 2 {
|
||||
// add a record for the NS, together with a stub A or AAAA record
|
||||
|
||||
var stubtype string
|
||||
if strings.IndexRune(fields[1], ':') == -1 {
|
||||
// no : so IPv4
|
||||
stubtype = "A"
|
||||
} else {
|
||||
// has : so IPv6
|
||||
stubtype = "AAAA"
|
||||
}
|
||||
|
||||
zone.AddRecord(name, "NS", fields[0]+".", comment)
|
||||
zone.AddRecord(fields[0], stubtype, fields[1], comment)
|
||||
|
||||
} else {
|
||||
// no, just add an NS record as it was presented
|
||||
zone.AddRecord(name, "NS", ns.RawValue+".", comment)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dsrdata := object.GetKey("ds-rdata")
|
||||
for _, ds := range dsrdata {
|
||||
zone.AddRecord(name, "DS", ds.RawValue, comment)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Functions for outputting zone records in different formats
|
||||
|
||||
func (r *DNSRecord) ToBindString() string {
|
||||
var comment string
|
||||
if r.Comment == "" {
|
||||
comment = ""
|
||||
} else {
|
||||
comment = "\t; " + r.Comment
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s\tIN\t%s\t%s%s",
|
||||
r.Name, r.Type, r.Content, comment,
|
||||
)
|
||||
}
|
||||
|
||||
func (zone *DNSZone) WriteBindFormat(w http.ResponseWriter) {
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
// provide a header
|
||||
fmt.Fprintf(w, ";; DN42 Root Zone Records\n"+
|
||||
";; Commit Reference: %s\n;; Generated: %s\n",
|
||||
zone.Commit, zone.Generated)
|
||||
|
||||
// then simply output each record in turn
|
||||
for _, record := range zone.Records {
|
||||
fmt.Fprintln(w, record.ToBindString())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
536
regapi.go
Normal file
536
regapi.go
Normal file
@ -0,0 +1,536 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry API Server
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
// "fmt"
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"strings"
|
||||
// "time"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// data structures
|
||||
|
||||
type RegMetaReturn struct {
|
||||
Commit string
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// register the api
|
||||
|
||||
func init() {
|
||||
EventBus.Listen("APIEndpoint", InitRegistryAPI)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// called from main to initialise the API routing
|
||||
|
||||
func InitRegistryAPI(params ...interface{}) {
|
||||
|
||||
router := params[0].(*mux.Router)
|
||||
|
||||
s := router.
|
||||
Methods("GET").
|
||||
PathPrefix("/registry").
|
||||
Subrouter()
|
||||
|
||||
s.HandleFunc("/", regRootHandler)
|
||||
//s.HandleFunc("/.schema", rTypeListHandler)
|
||||
//s.HandleFunc("/.meta/", rTypeListHandler)
|
||||
|
||||
s.HandleFunc("/.meta", regMetaHandler)
|
||||
s.HandleFunc("/{type}", regTypeHandler)
|
||||
s.HandleFunc("/{type}/{object}", regObjectHandler)
|
||||
s.HandleFunc("/{type}/{object}/{key}", regKeyHandler)
|
||||
s.HandleFunc("/{type}/{object}/{key}/{attribute}", regAttributeHandler)
|
||||
|
||||
log.Info("Registry API installed")
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// return registry metadata
|
||||
|
||||
func regMetaHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
rv := RegMetaReturn{
|
||||
Commit: RegistryData.Commit,
|
||||
}
|
||||
|
||||
ResponseJSON(w, rv)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// filter functions
|
||||
|
||||
// return a list of types that match the filter
|
||||
func filterTypes(filter string) []*RegType {
|
||||
|
||||
var rtypes []*RegType = nil
|
||||
|
||||
// check if filter starts with '*'
|
||||
if filter[0] == '*' {
|
||||
// try and match the filter against all reg types
|
||||
|
||||
filter = strings.ToLower(filter[1:])
|
||||
|
||||
// special case, if the filter was '*' return all types
|
||||
if len(filter) == 0 {
|
||||
|
||||
rtypes = make([]*RegType, 0, len(RegistryData.Types))
|
||||
for _, rtype := range RegistryData.Types {
|
||||
rtypes = append(rtypes, rtype)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// otherwise substring match the types
|
||||
for _, rtype := range RegistryData.Types {
|
||||
lname := strings.ToLower(rtype.Ref)
|
||||
if strings.Contains(lname, filter) {
|
||||
// matched, add it to the list
|
||||
rtypes = append(rtypes, rtype)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
// perform an exact match with one entry
|
||||
|
||||
rtype := RegistryData.Types[filter]
|
||||
if rtype != nil {
|
||||
// return a single answer
|
||||
rtypes = []*RegType{rtype}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return rtypes
|
||||
}
|
||||
|
||||
// return a list of objects from a set of types that match a filter
|
||||
func filterObjects(rtypes []*RegType, filter string) []*RegObject {
|
||||
|
||||
var objects []*RegObject = nil
|
||||
|
||||
// check if filter starts with '*'
|
||||
if filter[0] == '*' {
|
||||
// try and match objects against the filter
|
||||
|
||||
filter = strings.ToLower(filter[1:])
|
||||
|
||||
// for each type
|
||||
for _, rtype := range rtypes {
|
||||
|
||||
// special case, if the filter was '*' return all objects
|
||||
if len(filter) == 0 {
|
||||
|
||||
objs := make([]*RegObject, 0, len(rtype.Objects))
|
||||
for _, object := range rtype.Objects {
|
||||
objs = append(objs, object)
|
||||
}
|
||||
objects = append(objects, objs...)
|
||||
|
||||
} else {
|
||||
// otherwise substring match the object names
|
||||
|
||||
for _, object := range rtype.Objects {
|
||||
lname := strings.ToLower(object.Ref)
|
||||
if strings.Contains(lname, filter) {
|
||||
// matched, add it to the list
|
||||
objects = append(objects, object)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
// perform an exact match against one object for each type
|
||||
|
||||
for _, rtype := range rtypes {
|
||||
|
||||
object := rtype.Objects[filter]
|
||||
if object != nil {
|
||||
// add the object
|
||||
objects = append(objects, object)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return objects
|
||||
}
|
||||
|
||||
// return a list of key indices matching the filter
|
||||
func filterKeys(rtypes []*RegType, filter string) []*RegKeyIndex {
|
||||
|
||||
var ix []*RegKeyIndex = nil
|
||||
|
||||
// check if filter starts with '*'
|
||||
if filter[0] == '*' {
|
||||
// try and match keys against the filter
|
||||
|
||||
filter = strings.ToLower(filter[1:])
|
||||
|
||||
// for each type
|
||||
for _, rtype := range rtypes {
|
||||
ref := rtype.Ref
|
||||
schema := RegistryData.Schema[ref]
|
||||
|
||||
// special case, if the filter was '*' return all indices
|
||||
if len(filter) == 0 {
|
||||
|
||||
tmp := make([]*RegKeyIndex, 0, len(schema.KeyIndex))
|
||||
for _, keyix := range schema.KeyIndex {
|
||||
tmp = append(tmp, keyix)
|
||||
}
|
||||
ix = append(ix, tmp...)
|
||||
|
||||
} else {
|
||||
// otherwise substring match the key names
|
||||
|
||||
for kname, keyix := range schema.KeyIndex {
|
||||
kname = strings.ToLower(kname)
|
||||
if strings.Contains(kname, filter) {
|
||||
ix = append(ix, keyix)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// perform an exact match, one key for each type
|
||||
|
||||
for _, rtype := range rtypes {
|
||||
ref := rtype.Ref
|
||||
schema := RegistryData.Schema[ref]
|
||||
keyix := schema.KeyIndex[filter]
|
||||
if keyix != nil {
|
||||
// add the index
|
||||
ix = append(ix, keyix)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ix
|
||||
}
|
||||
|
||||
// helper func to determine if an attribute matches a filter
|
||||
func matchAttribute(attribute *RegAttribute,
|
||||
filter string, isExact bool) bool {
|
||||
|
||||
if isExact {
|
||||
|
||||
return filter == attribute.RawValue
|
||||
|
||||
} else {
|
||||
|
||||
l := strings.ToLower(attribute.RawValue)
|
||||
return strings.Contains(l, filter)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// return a map of objects and attribute values that match the filter
|
||||
func filterAttributes(ix []*RegKeyIndex, objects []*RegObject,
|
||||
filter string, raw bool) map[string]map[string][]string {
|
||||
|
||||
result := make(map[string]map[string][]string)
|
||||
|
||||
// pre-calculate the search type
|
||||
isExact := true
|
||||
isAll := false
|
||||
|
||||
if filter[0] == '*' {
|
||||
isExact = false
|
||||
filter = strings.ToLower(filter[1:])
|
||||
if len(filter) == 0 {
|
||||
isAll = true
|
||||
}
|
||||
}
|
||||
|
||||
// for each key index
|
||||
for _, keyix := range ix {
|
||||
|
||||
// for each object
|
||||
for _, object := range objects {
|
||||
|
||||
// attributes in this object that match this key
|
||||
attributes := keyix.Objects[object]
|
||||
if attributes != nil {
|
||||
// this object has at least one relevant key
|
||||
|
||||
// match the attributes
|
||||
for _, attribute := range attributes {
|
||||
if isAll || matchAttribute(attribute, filter, isExact) {
|
||||
// match found !
|
||||
|
||||
objmap := result[object.Ref]
|
||||
if objmap == nil {
|
||||
objmap = make(map[string][]string)
|
||||
result[object.Ref] = objmap
|
||||
}
|
||||
|
||||
// append the result
|
||||
var value *string
|
||||
if raw {
|
||||
value = &attribute.RawValue
|
||||
} else {
|
||||
value = &attribute.Value
|
||||
}
|
||||
|
||||
objmap[keyix.Ref] = append(objmap[keyix.Ref], *value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// root handler, lists all types within the registry
|
||||
|
||||
func regRootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
response := make(map[string]int)
|
||||
for _, rType := range RegistryData.Types {
|
||||
response[rType.Ref] = len(rType.Objects)
|
||||
}
|
||||
ResponseJSON(w, response)
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// type handler returns list of objects that match the type
|
||||
|
||||
func regTypeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// request parameters
|
||||
vars := mux.Vars(r)
|
||||
tFilter := vars["type"] // type filter
|
||||
|
||||
// match registry types against the filter
|
||||
rtypes := filterTypes(tFilter)
|
||||
if rtypes == nil {
|
||||
http.Error(w, "No objects matching '"+tFilter+"' found",
|
||||
http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// construct the response
|
||||
response := make(map[string][]string)
|
||||
for _, rtype := range rtypes {
|
||||
|
||||
objects := make([]string, 0, len(rtype.Objects))
|
||||
for key := range rtype.Objects {
|
||||
objects = append(objects, key)
|
||||
}
|
||||
|
||||
response[rtype.Ref] = objects
|
||||
}
|
||||
|
||||
ResponseJSON(w, response)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// object handler returns object data
|
||||
|
||||
// per object response structure
|
||||
type RegObjectResponse struct {
|
||||
Attributes [][2]string
|
||||
Backlinks []string
|
||||
}
|
||||
|
||||
func regObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// request parameters
|
||||
vars := mux.Vars(r)
|
||||
query := r.URL.Query()
|
||||
|
||||
tFilter := vars["type"] // type filter
|
||||
oFilter := vars["object"] // object filter
|
||||
raw := query["raw"] // raw or decorated results
|
||||
|
||||
// select the type(s)
|
||||
rtypes := filterTypes(tFilter)
|
||||
if rtypes == nil {
|
||||
http.Error(w, "No objects matching '"+tFilter+"' found",
|
||||
http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// then select the objects
|
||||
objects := filterObjects(rtypes, oFilter)
|
||||
if objects == nil {
|
||||
http.Error(w, "No objects matching '"+tFilter+
|
||||
"/"+oFilter+"' found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// collate the results in to the response data
|
||||
if raw == nil {
|
||||
// provide a decorated response
|
||||
response := make(map[string]RegObjectResponse)
|
||||
|
||||
// for each object in the results
|
||||
for _, object := range objects {
|
||||
|
||||
// copy the raw attributes
|
||||
attributes := make([][2]string, len(object.Data))
|
||||
for ix, attribute := range object.Data {
|
||||
attributes[ix] = [2]string{attribute.Key, attribute.Value}
|
||||
}
|
||||
|
||||
// construct the backlinks
|
||||
backlinks := make([]string, len(object.Backlinks))
|
||||
for ix, object := range object.Backlinks {
|
||||
backlinks[ix] = object.Ref
|
||||
}
|
||||
|
||||
// add to the response
|
||||
response[object.Ref] = RegObjectResponse{
|
||||
Attributes: attributes,
|
||||
Backlinks: backlinks,
|
||||
}
|
||||
}
|
||||
|
||||
ResponseJSON(w, response)
|
||||
|
||||
} else {
|
||||
// provide a response with just the raw registry data
|
||||
response := make(map[string][][2]string)
|
||||
|
||||
// for each object in the results
|
||||
for _, object := range objects {
|
||||
|
||||
attributes := make([][2]string, len(object.Data))
|
||||
response[object.Ref] = attributes
|
||||
|
||||
// copy the raw attributes
|
||||
for ix, attribute := range object.Data {
|
||||
attributes[ix] = [2]string{attribute.Key, attribute.RawValue}
|
||||
}
|
||||
}
|
||||
|
||||
ResponseJSON(w, response)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// key handler returns attribute data matching the key
|
||||
|
||||
func regKeyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// request parameters
|
||||
vars := mux.Vars(r)
|
||||
query := r.URL.Query()
|
||||
|
||||
tFilter := vars["type"] // type filter
|
||||
oFilter := vars["object"] // object filter
|
||||
kFilter := vars["key"] // key filter
|
||||
raw := query["raw"] // raw or decorated results
|
||||
|
||||
// select the type(s)
|
||||
rtypes := filterTypes(tFilter)
|
||||
if rtypes == nil {
|
||||
http.Error(w, "No objects matching '"+tFilter+"' found",
|
||||
http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// select the key indices
|
||||
ix := filterKeys(rtypes, kFilter)
|
||||
if rtypes == nil {
|
||||
http.Error(w, "No objects matching '"+tFilter+"/*/"+
|
||||
kFilter+"' found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// select the objects
|
||||
objects := filterObjects(rtypes, oFilter)
|
||||
if objects == nil {
|
||||
http.Error(w, "No objects matching '"+tFilter+
|
||||
"/"+oFilter+"' found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// select objects that match the keys
|
||||
amap := filterAttributes(ix, objects, "*", (raw != nil))
|
||||
if len(amap) == 0 {
|
||||
http.Error(w, "No attributes matching '"+tFilter+"/"+
|
||||
oFilter+"/"+kFilter+"' found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ResponseJSON(w, amap)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// attribute handler returns attribute data matching the attribute
|
||||
|
||||
func regAttributeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// request parameters
|
||||
vars := mux.Vars(r)
|
||||
query := r.URL.Query()
|
||||
|
||||
tFilter := vars["type"] // type filter
|
||||
oFilter := vars["object"] // object filter
|
||||
kFilter := vars["key"] // key filter
|
||||
aFilter := vars["attribute"] // attribute filter
|
||||
raw := query["raw"] // raw or decorated results
|
||||
|
||||
// select the type(s)
|
||||
rtypes := filterTypes(tFilter)
|
||||
if rtypes == nil {
|
||||
http.Error(w, "No objects matching '"+tFilter+"' found",
|
||||
http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// select the key indices
|
||||
ix := filterKeys(rtypes, kFilter)
|
||||
if rtypes == nil {
|
||||
http.Error(w, "No objects matching '"+tFilter+"/*/"+
|
||||
kFilter+"' found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// then select the objects
|
||||
objects := filterObjects(rtypes, oFilter)
|
||||
if objects == nil {
|
||||
http.Error(w, "No objects matching '"+tFilter+
|
||||
"/"+oFilter+"' found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// select objects that match the keys
|
||||
amap := filterAttributes(ix, objects, aFilter, (raw != nil))
|
||||
if len(amap) == 0 {
|
||||
http.Error(w, "No attributes matching '"+tFilter+"/"+
|
||||
oFilter+"/"+kFilter+"/"+aFilter+"' found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ResponseJSON(w, amap)
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
717
registry.go
Normal file
717
registry.go
Normal file
@ -0,0 +1,717 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry API Server
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
// "errors"
|
||||
"fmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// registry data model
|
||||
|
||||
// registry data
|
||||
|
||||
// Attributes within Objects
|
||||
type RegAttribute struct {
|
||||
Key string
|
||||
Value string // this is a post-processed, or decorated value
|
||||
RawValue string // the raw value as read from the registry
|
||||
}
|
||||
|
||||
type RegObject struct {
|
||||
Ref string // the ref contains the full path for this object
|
||||
Data []*RegAttribute // the key/value data for this object
|
||||
Backlinks []*RegObject // other objects that reference this one
|
||||
}
|
||||
|
||||
// types are collections of objects
|
||||
type RegType struct {
|
||||
Ref string // full path for this type
|
||||
Objects map[string]*RegObject // the objects in this type
|
||||
}
|
||||
|
||||
// registry meta data
|
||||
|
||||
type RegAttributeSchema struct {
|
||||
Fields []string
|
||||
Relations []*RegType
|
||||
}
|
||||
|
||||
type RegKeyIndex struct {
|
||||
Ref string
|
||||
Objects map[*RegObject][]*RegAttribute
|
||||
}
|
||||
|
||||
type RegTypeSchema struct {
|
||||
Ref string
|
||||
Attributes map[string]*RegAttributeSchema
|
||||
KeyIndex map[string]*RegKeyIndex
|
||||
}
|
||||
|
||||
// the registry itself
|
||||
|
||||
type Registry struct {
|
||||
Commit string
|
||||
Schema map[string]*RegTypeSchema
|
||||
Types map[string]*RegType
|
||||
}
|
||||
|
||||
// and a variable for the actual data
|
||||
var RegistryData *Registry
|
||||
|
||||
// store the current commit has
|
||||
var previousCommit string
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// utility and manipulation functions
|
||||
|
||||
// general functions
|
||||
|
||||
func RegistryMakePath(t string, o string) string {
|
||||
return t + "/" + o
|
||||
}
|
||||
|
||||
func RegistrySplitPath(p string) (string, string) {
|
||||
tmp := strings.Split(p, "/")
|
||||
if len(tmp) != 2 {
|
||||
return "", ""
|
||||
}
|
||||
return tmp[0], tmp[1]
|
||||
}
|
||||
|
||||
func (registry *Registry) GetObject(path string) *RegObject {
|
||||
rtname, objname := RegistrySplitPath(path)
|
||||
rtype := registry.Types[rtname]
|
||||
if rtype == nil {
|
||||
return nil
|
||||
}
|
||||
return rtype.Objects[objname]
|
||||
}
|
||||
|
||||
// attribute functions
|
||||
|
||||
// nothing here
|
||||
|
||||
// object functions
|
||||
|
||||
// return attributes exactly matching a specific key
|
||||
func (object *RegObject) GetKey(key string) []*RegAttribute {
|
||||
|
||||
attributes := make([]*RegAttribute, 0)
|
||||
for _, a := range object.Data {
|
||||
if a.Key == key {
|
||||
attributes = append(attributes, a)
|
||||
}
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
// return a single key
|
||||
func (object *RegObject) GetSingleKey(key string) *RegAttribute {
|
||||
|
||||
attributes := object.GetKey(key)
|
||||
if len(attributes) != 1 {
|
||||
log.WithFields(log.Fields{
|
||||
"key": key,
|
||||
"object": object.Ref,
|
||||
}).Error("Unable to find unique key in object")
|
||||
|
||||
// can't register the object
|
||||
return nil
|
||||
}
|
||||
return attributes[0]
|
||||
}
|
||||
|
||||
// schema functions
|
||||
|
||||
// validate a set of attributes against a schema
|
||||
func (schema *RegTypeSchema) validate(attributes []*RegAttribute) []*RegAttribute {
|
||||
|
||||
validated := make([]*RegAttribute, 0, len(attributes))
|
||||
for _, attribute := range attributes {
|
||||
|
||||
// keys beginning with 'x-' are user defined, skip validation
|
||||
if !strings.HasPrefix(attribute.Key, "x-") {
|
||||
if schema.Attributes[attribute.Key] == nil {
|
||||
// couldn't find a schema attribute
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"key": attribute.Key,
|
||||
"schema": schema.Ref,
|
||||
}).Error("Schema validation failed")
|
||||
|
||||
// don't add to the validated list
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// all ok
|
||||
validated = append(validated, attribute)
|
||||
}
|
||||
|
||||
return validated
|
||||
}
|
||||
|
||||
// add an attribute to the key map
|
||||
func (schema *RegTypeSchema) addKeyIndex(object *RegObject,
|
||||
attribute *RegAttribute) {
|
||||
|
||||
keyix := schema.KeyIndex[attribute.Key]
|
||||
// create a new object map if it didn't exist
|
||||
if keyix == nil {
|
||||
keyix = &RegKeyIndex{
|
||||
Ref: attribute.Key,
|
||||
Objects: make(map[*RegObject][]*RegAttribute),
|
||||
}
|
||||
schema.KeyIndex[attribute.Key] = keyix
|
||||
}
|
||||
|
||||
// add the object/attribute reference
|
||||
keyix.Objects[object] = append(keyix.Objects[object], attribute)
|
||||
}
|
||||
|
||||
// object functions
|
||||
|
||||
// add a backlink to an object
|
||||
func (object *RegObject) addBacklink(ref *RegObject) {
|
||||
|
||||
// check if the backlink already exists, this could be the case
|
||||
// if an object is referenced multiple times (e.g. admin-c & tech-c)
|
||||
for _, blink := range object.Backlinks {
|
||||
if blink == ref {
|
||||
// already exists, just return as nothing to do
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// didn't find a match, add the backlink
|
||||
object.Backlinks = append(object.Backlinks, ref)
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// reload the registry
|
||||
|
||||
func reloadRegistry(path string, commit string) {
|
||||
|
||||
log.Debug("Reloading registry")
|
||||
|
||||
// r will become the new registry data
|
||||
registry := &Registry{
|
||||
Commit: commit,
|
||||
Schema: make(map[string]*RegTypeSchema),
|
||||
Types: make(map[string]*RegType),
|
||||
}
|
||||
|
||||
// bootstrap the schema registry type
|
||||
registry.Types["schema"] = &RegType{
|
||||
Ref: "schema",
|
||||
Objects: make(map[string]*RegObject),
|
||||
}
|
||||
registry.loadType("schema", path)
|
||||
|
||||
// and parse the schema to get the remaining types
|
||||
registry.parseSchema()
|
||||
|
||||
// now load the remaining types
|
||||
for _, rType := range registry.Types {
|
||||
registry.loadType(rType.Ref, path)
|
||||
}
|
||||
|
||||
// mark relationships
|
||||
registry.decorate()
|
||||
|
||||
// trigger updates in any other modules
|
||||
EventBus.Fire("RegistryUpdate", registry, path)
|
||||
|
||||
// swap in the new registry data
|
||||
RegistryData = registry
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// create and load the raw data for a registry type
|
||||
|
||||
func (registry *Registry) loadType(typeName string, path string) {
|
||||
|
||||
// the type will already have been created
|
||||
rType := registry.Types[typeName]
|
||||
|
||||
// as will the schema (unless attempting to load the schema itself)
|
||||
schema := registry.Schema[typeName]
|
||||
|
||||
// special case for DNS as the directory
|
||||
// doesn't match the type name
|
||||
if typeName == "domain" {
|
||||
path += "/dns"
|
||||
} else {
|
||||
path += "/" + typeName
|
||||
}
|
||||
|
||||
// and load all the objects in this type
|
||||
rType.loadObjects(schema, path)
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// load all the objects associated with a type
|
||||
|
||||
func (rType *RegType) loadObjects(schema *RegTypeSchema, path string) {
|
||||
|
||||
entries, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": path,
|
||||
"type": rType.Ref,
|
||||
}).Error("Failed to read registry type directory")
|
||||
return
|
||||
}
|
||||
|
||||
// for each entry in the directory
|
||||
for _, entry := range entries {
|
||||
|
||||
// each file maps to a registry object
|
||||
if !entry.IsDir() {
|
||||
|
||||
filename := entry.Name()
|
||||
// ignore dotfiles
|
||||
if !strings.HasPrefix(filename, ".") {
|
||||
|
||||
// load the attributes from file
|
||||
attributes := loadAttributes(path + "/" + filename)
|
||||
|
||||
// basic validation of attributes against the schema
|
||||
// schema may be nil if we are actually loading the schema itself
|
||||
if schema != nil {
|
||||
attributes = schema.validate(attributes)
|
||||
}
|
||||
|
||||
// make the object
|
||||
object := &RegObject{
|
||||
Ref: RegistryMakePath(rType.Ref, filename),
|
||||
Data: attributes,
|
||||
Backlinks: make([]*RegObject, 0),
|
||||
}
|
||||
|
||||
// add to type
|
||||
rType.Objects[filename] = object
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"ref": rType.Ref,
|
||||
"path": path,
|
||||
"count": len(rType.Objects),
|
||||
}).Debug("Loaded registry type")
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// read attributes from a file
|
||||
|
||||
func loadAttributes(path string) []*RegAttribute {
|
||||
|
||||
attributes := make([]*RegAttribute, 0)
|
||||
|
||||
// open the file to start reading it
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": path,
|
||||
}).Error("Failed to read attributes from file")
|
||||
return attributes
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// read the file line by line using the bufio scanner
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
|
||||
line := strings.TrimRight(scanner.Text(), "\r\n")
|
||||
|
||||
// lines starting with '+' denote an empty line
|
||||
if line[0] == '+' {
|
||||
|
||||
// concatenate a \n on to the previous attribute value
|
||||
attributes[len(attributes)-1].RawValue += "\n"
|
||||
|
||||
} else {
|
||||
|
||||
// look for a : separator in the first 20 characters
|
||||
ix := strings.IndexByte(line, ':')
|
||||
if ix == -1 || ix >= 20 {
|
||||
// couldn't find one
|
||||
|
||||
if len(line) <= 20 {
|
||||
// hmmm, the line was shorter than 20 characters
|
||||
// something is amiss
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"length": len(line),
|
||||
"path": path,
|
||||
"line": line,
|
||||
}).Warn("Short line detected")
|
||||
|
||||
} else {
|
||||
|
||||
// line is a continuation of the previous line, so
|
||||
// concatenate the value on to the previous attribute value
|
||||
attributes[len(attributes)-1].RawValue +=
|
||||
"\n" + string(line[20:])
|
||||
|
||||
}
|
||||
} else {
|
||||
// found a key and : separator
|
||||
|
||||
// is there actually a value ?
|
||||
var value string
|
||||
if len(line) <= 20 {
|
||||
// blank value
|
||||
value = ""
|
||||
} else {
|
||||
value = string(line[20:])
|
||||
}
|
||||
|
||||
// create a new attribute
|
||||
a := &RegAttribute{
|
||||
Key: string(line[:ix]),
|
||||
RawValue: value,
|
||||
}
|
||||
attributes = append(attributes, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// parse schema files to extract keys and for attribute relations
|
||||
|
||||
func (registry *Registry) parseSchema() {
|
||||
|
||||
// for each object in the schema type
|
||||
for _, object := range registry.Types["schema"].Objects {
|
||||
|
||||
// look up the ref attribute
|
||||
ref := object.GetSingleKey("ref")
|
||||
if ref == nil {
|
||||
log.WithFields(log.Fields{
|
||||
"object": object.Ref,
|
||||
}).Error("Schema record without ref")
|
||||
|
||||
// can't process this object
|
||||
continue
|
||||
}
|
||||
|
||||
// create the type schema object
|
||||
typeName := strings.TrimPrefix(ref.RawValue, "dn42.")
|
||||
typeSchema := &RegTypeSchema{
|
||||
Ref: typeName,
|
||||
Attributes: make(map[string]*RegAttributeSchema),
|
||||
KeyIndex: make(map[string]*RegKeyIndex),
|
||||
}
|
||||
|
||||
// ensure the type exists
|
||||
rType := registry.Types[typeName]
|
||||
if rType == nil {
|
||||
rType := &RegType{
|
||||
Ref: typeName,
|
||||
Objects: make(map[string]*RegObject),
|
||||
}
|
||||
registry.Types[typeName] = rType
|
||||
}
|
||||
|
||||
// for each key attribute in the schema
|
||||
attributes := object.GetKey("key")
|
||||
for _, attribute := range attributes {
|
||||
|
||||
// split the value on whitespace
|
||||
fields := strings.Fields(attribute.RawValue)
|
||||
keyName := fields[0]
|
||||
|
||||
typeSchema.Attributes[keyName] = &RegAttributeSchema{
|
||||
Fields: fields[1:],
|
||||
}
|
||||
}
|
||||
|
||||
// register the type schema
|
||||
registry.Schema[typeName] = typeSchema
|
||||
|
||||
}
|
||||
|
||||
// scan the fields of each schema attribute to determine relationships
|
||||
// this needs to be second step to allow pre-creation of the types
|
||||
for _, typeSchema := range registry.Schema {
|
||||
for attribName, attribSchema := range typeSchema.Attributes {
|
||||
for _, field := range attribSchema.Fields {
|
||||
if strings.HasPrefix(field, "lookup=") {
|
||||
|
||||
// the relationships may be a multivalue, separated by ,
|
||||
rels := strings.Split(strings.
|
||||
TrimPrefix(field, "lookup="), ",")
|
||||
|
||||
// map to a regtype
|
||||
relations := make([]*RegType, 0, len(rels))
|
||||
for ix := range rels {
|
||||
relName := strings.TrimPrefix(rels[ix], "dn42.")
|
||||
relation := registry.Types[relName]
|
||||
|
||||
// log if unable to look up the type
|
||||
if relation == nil {
|
||||
// log unless this is the schema def lookup=str '>' [spec]...
|
||||
if typeSchema.Ref != "schema" {
|
||||
log.WithFields(log.Fields{
|
||||
"relation": relName,
|
||||
"attribute": attribName,
|
||||
"type": typeSchema.Ref,
|
||||
}).Error("Relation to type that does not exist")
|
||||
}
|
||||
|
||||
} else {
|
||||
// store the relationship
|
||||
relations = append(relations, relation)
|
||||
}
|
||||
}
|
||||
|
||||
// register the relations
|
||||
attribSchema.Relations = relations
|
||||
|
||||
// assume only 1 lookup= per key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("Schema parsing complete")
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// parse all attributes and decorate them
|
||||
|
||||
func (registry *Registry) decorate() {
|
||||
|
||||
cattribs := 0
|
||||
cmatched := 0
|
||||
|
||||
// walk each attribute value
|
||||
for _, rType := range registry.Types {
|
||||
schema := registry.Schema[rType.Ref]
|
||||
for _, object := range rType.Objects {
|
||||
for _, attribute := range object.Data {
|
||||
cattribs += 1
|
||||
|
||||
// add this attribute to the key map
|
||||
schema.addKeyIndex(object, attribute)
|
||||
|
||||
attribSchema := schema.Attributes[attribute.Key]
|
||||
// are there relations defined for this attribute ?
|
||||
// attribSchema may be null if this attribute is user defined (x-*)
|
||||
if (attribSchema != nil) &&
|
||||
attribute.matchRelation(object, attribSchema.Relations) {
|
||||
// matched
|
||||
cmatched += 1
|
||||
} else {
|
||||
// no match, just copy the attribute data
|
||||
attribute.Value = attribute.RawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"attributes": cattribs,
|
||||
"matched": cmatched,
|
||||
}).Debug("Decoration complete")
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// match an attribute against schema relations
|
||||
|
||||
func (attribute *RegAttribute) matchRelation(parent *RegObject,
|
||||
relations []*RegType) bool {
|
||||
|
||||
// it's not going to match if relations is empty
|
||||
if relations == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// check each relation
|
||||
for _, relation := range relations {
|
||||
|
||||
object := relation.Objects[attribute.RawValue]
|
||||
if object != nil {
|
||||
// found a match !
|
||||
|
||||
// decorate the attribute value
|
||||
attribute.Value = fmt.Sprintf("[%s](%s)",
|
||||
attribute.RawValue, object.Ref)
|
||||
|
||||
// and add a back reference to the related object
|
||||
object.addBacklink(parent)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// didn't find anything
|
||||
return false
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// fetch the current commit hash
|
||||
|
||||
func getCommitHash(regDir string, gitPath string) string {
|
||||
|
||||
// run git to get the latest commit hash
|
||||
cmd := exec.Command(gitPath, "log", "-1", "--format=%H")
|
||||
cmd.Dir = regDir
|
||||
// execute
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"gitPath": gitPath,
|
||||
"regDir": regDir,
|
||||
}).Error("Failed to execute git log")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// refresh the registry
|
||||
|
||||
func refreshRegistry(regDir string, gitPath string, branch string) {
|
||||
|
||||
// run git fetch to get the current commits from the master
|
||||
cmd := exec.Command(gitPath, "fetch")
|
||||
cmd.Dir = regDir
|
||||
// execute
|
||||
if out, err := cmd.Output(); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"gitPath": gitPath,
|
||||
"regDir": regDir,
|
||||
}).Error("Failed to execute git fetch")
|
||||
} else {
|
||||
fmt.Printf("Git Fetch: %s", string(out))
|
||||
}
|
||||
|
||||
// then reset hard to match the master
|
||||
cmd = exec.Command(gitPath, "reset", "--hard", "origin/"+branch)
|
||||
cmd.Dir = regDir
|
||||
// execute
|
||||
if out, err := cmd.Output(); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"gitPath": gitPath,
|
||||
"regDir": regDir,
|
||||
"branch": branch,
|
||||
}).Error("Failed to execute git reset")
|
||||
} else {
|
||||
fmt.Printf("Git Reset: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// called from main to initialse the registry data and syncing
|
||||
|
||||
func InitialiseRegistryData(regDir string, refresh time.Duration,
|
||||
gitPath string, autoPull bool, branch string) {
|
||||
|
||||
// validate that the regDir/data path exists
|
||||
dataPath := regDir + "/data"
|
||||
regStat, err := os.Stat(dataPath)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": dataPath,
|
||||
}).Fatal("Unable to find registry directory")
|
||||
}
|
||||
|
||||
// and it is a directory
|
||||
if !regStat.IsDir() {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": dataPath,
|
||||
}).Fatal("Registry path is not a directory")
|
||||
}
|
||||
|
||||
// check that git exists
|
||||
_, err = os.Stat(gitPath)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": gitPath,
|
||||
}).Fatal("Unable to find git executable")
|
||||
}
|
||||
|
||||
// enforce a minimum update time
|
||||
minTime := 10 * time.Minute
|
||||
if refresh < minTime {
|
||||
log.WithFields(log.Fields{
|
||||
"interval": refresh,
|
||||
}).Error("Enforcing minimum update time of 10 minutes")
|
||||
|
||||
refresh = minTime
|
||||
}
|
||||
|
||||
// initialise the previous commit hash
|
||||
// and do initial load from registry
|
||||
previousCommit = getCommitHash(regDir, gitPath)
|
||||
reloadRegistry(dataPath, previousCommit)
|
||||
|
||||
go func() {
|
||||
|
||||
// every refresh interval
|
||||
for range time.Tick(refresh) {
|
||||
log.Debug("Refresh Timer")
|
||||
|
||||
// automatically try to refresh the registry ?
|
||||
if autoPull {
|
||||
refreshRegistry(regDir, gitPath, branch)
|
||||
}
|
||||
|
||||
// get the latest hash
|
||||
currentCommit := getCommitHash(regDir, gitPath)
|
||||
|
||||
// has the registry been updated ?
|
||||
if currentCommit != previousCommit {
|
||||
log.WithFields(log.Fields{
|
||||
"current": currentCommit,
|
||||
"previous": previousCommit,
|
||||
}).Info("Registry has changed, refresh started")
|
||||
|
||||
// refresh
|
||||
reloadRegistry(dataPath, currentCommit)
|
||||
|
||||
// update commit
|
||||
previousCommit = currentCommit
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
471
roaapi.go
Normal file
471
roaapi.go
Normal file
@ -0,0 +1,471 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry API Server
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
// "math/big"
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// register the api
|
||||
|
||||
func init() {
|
||||
EventBus.Listen("APIEndpoint", InitROAAPI)
|
||||
EventBus.Listen("RegistryUpdate", ROAUpdate)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// data model
|
||||
|
||||
type PrefixROA struct {
|
||||
Prefix string `json:"prefix"`
|
||||
MaxLen uint8 `json:"maxLength"`
|
||||
ASN string `json:"asn"`
|
||||
}
|
||||
|
||||
type ROAFilter struct {
|
||||
Number uint `json:"nr"`
|
||||
Action string `json:"action"`
|
||||
Prefix string `json:"prefix"`
|
||||
MinLen uint8 `json:"minlen"`
|
||||
MaxLen uint8 `json:"maxlen"`
|
||||
Network *net.IPNet `json:"-"`
|
||||
IPType uint8 `json:"-"`
|
||||
}
|
||||
|
||||
type ROA struct {
|
||||
CTime time.Time
|
||||
Commit string
|
||||
Filters []*ROAFilter
|
||||
IPv4 []*PrefixROA
|
||||
IPv6 []*PrefixROA
|
||||
}
|
||||
|
||||
var ROAData *ROA
|
||||
|
||||
// set validity period for one week
|
||||
// this might appear to be a long time, but is intended to provide
|
||||
// enough time to prevent expiry of the data between real registry
|
||||
// updates (which may only happen infrequently)
|
||||
const ROA_JSON_VALIDITY_PERIOD = (7 * 24)
|
||||
|
||||
type ROAMetaData struct {
|
||||
Counts uint `json:"counts"`
|
||||
Generated uint32 `json:"generated"`
|
||||
Valid uint32 `json:"valid"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
SignatureDate string `json:"signatureDate,omitempty"`
|
||||
}
|
||||
|
||||
type ROAJSON struct {
|
||||
MetaData ROAMetaData `json:"metadata"`
|
||||
Roas []*PrefixROA `json:"roas"`
|
||||
}
|
||||
|
||||
var ROAJSONResponse *ROAJSON
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// called from main to initialise the API routing
|
||||
|
||||
func InitROAAPI(params ...interface{}) {
|
||||
|
||||
router := params[0].(*mux.Router)
|
||||
|
||||
s := router.
|
||||
Methods("GET").
|
||||
PathPrefix("/roa").
|
||||
Subrouter()
|
||||
|
||||
s.HandleFunc("/filter/{ipv}", roaFilterHandler)
|
||||
s.HandleFunc("/json", roaJSONHandler)
|
||||
s.HandleFunc("/bird/{birdv}/{ipv}", roaBirdHandler)
|
||||
|
||||
log.Info("ROA API installed")
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// api handlers
|
||||
|
||||
// return JSON formatted version of filter{,6}.txt
|
||||
func roaFilterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
vars := mux.Vars(r)
|
||||
ipv := vars["ipv"]
|
||||
|
||||
// pre-create an array to hold the result
|
||||
filters := make([]*ROAFilter, 0, len(ROAData.Filters))
|
||||
|
||||
// helper closure to select from the filter array
|
||||
fselect := func(a []*ROAFilter, t uint8) []*ROAFilter {
|
||||
for _, f := range ROAData.Filters {
|
||||
if f.IPType == t {
|
||||
a = append(a, f)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// add ipv4 filters if required
|
||||
if strings.ContainsRune(ipv, '4') {
|
||||
filters = fselect(filters, 4)
|
||||
}
|
||||
|
||||
// add ipv6 filters if required
|
||||
if strings.ContainsRune(ipv, '6') {
|
||||
filters = fselect(filters, 6)
|
||||
}
|
||||
|
||||
ResponseJSON(w, filters)
|
||||
}
|
||||
|
||||
// return JSON formatted ROA data suitable for use with GoRTR
|
||||
func roaJSONHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// check validity period of returned data
|
||||
tnow := uint32(time.Now().Unix())
|
||||
valid := ROAJSONResponse.MetaData.Valid
|
||||
|
||||
// check if validity period is close to expiry
|
||||
if (tnow > valid) ||
|
||||
((valid - tnow) < (ROA_JSON_VALIDITY_PERIOD / 4)) {
|
||||
// if so extend the validity period
|
||||
ROAJSONResponse.MetaData.Valid += (ROA_JSON_VALIDITY_PERIOD * 3600)
|
||||
}
|
||||
|
||||
ResponseJSON(w, ROAJSONResponse)
|
||||
}
|
||||
|
||||
// return the roa in bird format
|
||||
func roaBirdHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
vars := mux.Vars(r)
|
||||
birdv := vars["birdv"]
|
||||
ipv := vars["ipv"]
|
||||
|
||||
// bird 1 or bird 2 format
|
||||
birdf := "roa %s max %d as %s;\n"
|
||||
if birdv == "2" {
|
||||
birdf = "route %s max %d as %s;\n"
|
||||
}
|
||||
|
||||
var roa []*PrefixROA
|
||||
if strings.ContainsRune(ipv, '4') {
|
||||
roa = append(roa, ROAData.IPv4...)
|
||||
}
|
||||
if strings.ContainsRune(ipv, '6') {
|
||||
roa = append(roa, ROAData.IPv6...)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
fmt.Fprintf(w, "#\n# dn42regsrv ROA Generator\n# Last Updated: %s\n"+
|
||||
"# Commit: %s\n#\n", ROAData.CTime.String(), ROAData.Commit)
|
||||
|
||||
for _, r := range roa {
|
||||
fmt.Fprintf(w, birdf, r.Prefix, r.MaxLen, r.ASN[2:])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// called whenever the registry is updated
|
||||
|
||||
func ROAUpdate(params ...interface{}) {
|
||||
|
||||
registry := params[0].(*Registry)
|
||||
path := params[1].(string)
|
||||
|
||||
// initiate new ROA data
|
||||
roa := &ROA{
|
||||
CTime: time.Now(),
|
||||
Commit: registry.Commit,
|
||||
}
|
||||
|
||||
// load filter{,6}.txt files
|
||||
if roa.loadFilter(path+"/filter.txt", 4) != nil {
|
||||
// error loading IPv4 filter, don't update
|
||||
return
|
||||
}
|
||||
|
||||
if roa.loadFilter(path+"/filter6.txt", 6) != nil {
|
||||
// error loading IPv6 filter, don't update
|
||||
return
|
||||
}
|
||||
|
||||
// compile ROA prefixes
|
||||
roa.IPv4 = roa.CompileROA(registry, "route")
|
||||
roa.IPv6 = roa.CompileROA(registry, "route6")
|
||||
|
||||
// swap in the new data
|
||||
ROAData = roa
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"ipv4": len(roa.IPv4),
|
||||
"ipv6": len(roa.IPv6),
|
||||
}).Debug("ROA data updated")
|
||||
|
||||
// pre-compute the JSON return struct
|
||||
|
||||
utime := uint32(roa.CTime.Unix())
|
||||
|
||||
response := &ROAJSON{
|
||||
MetaData: ROAMetaData{
|
||||
Generated: utime,
|
||||
Valid: utime + (ROA_JSON_VALIDITY_PERIOD * 3600),
|
||||
},
|
||||
}
|
||||
|
||||
response.Roas = append(roa.IPv4, roa.IPv6...)
|
||||
response.MetaData.Counts = uint(len(response.Roas))
|
||||
|
||||
ROAJSONResponse = response
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// load network filter definitions from a filter file
|
||||
|
||||
func (roa *ROA) loadFilter(path string, iptype uint8) error {
|
||||
|
||||
// open the file for reading
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"path": path,
|
||||
"error": err,
|
||||
}).Error("Unable to open filter file")
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// helper closure to convert strings to numbers
|
||||
var cerr error
|
||||
convert := func(s string) int {
|
||||
if cerr != nil {
|
||||
return 0
|
||||
}
|
||||
val, cerr := strconv.Atoi(s)
|
||||
if cerr != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"number": s,
|
||||
"error": err,
|
||||
}).Error("Unable to parse number in filter file")
|
||||
return 0
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
filters := make([]*ROAFilter, 0)
|
||||
|
||||
// read the file line by line
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// remove any comments
|
||||
if ix := strings.IndexRune(line, '#'); ix != -1 {
|
||||
line = line[:ix]
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 5 {
|
||||
|
||||
// parse the prefix in to a NetIP structure
|
||||
prefix := fields[2]
|
||||
_, network, err := net.ParseCIDR(prefix)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"path": path,
|
||||
"prefix": prefix,
|
||||
"error": err,
|
||||
}).Error("Unable to parse CIDR in filter file")
|
||||
|
||||
} else {
|
||||
|
||||
// construct the filter object
|
||||
roaf := &ROAFilter{
|
||||
Number: uint(convert(fields[0])),
|
||||
Action: fields[1],
|
||||
Prefix: prefix,
|
||||
MinLen: uint8(convert(fields[3])),
|
||||
MaxLen: uint8(convert(fields[4])),
|
||||
Network: network,
|
||||
IPType: iptype,
|
||||
}
|
||||
|
||||
// add to list if no strconv error
|
||||
if cerr == nil {
|
||||
filters = append(filters, roaf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// did something go wrong ?
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"path": path,
|
||||
"error": err,
|
||||
}).Error("Scanner error reading filter file")
|
||||
return err
|
||||
}
|
||||
|
||||
// filter.txt should be in order,
|
||||
// but still sort by number just in case
|
||||
sort.Slice(filters, func(i, j int) bool {
|
||||
return filters[i].Number < filters[j].Number
|
||||
})
|
||||
|
||||
// add to the roa object
|
||||
roa.Filters = append(roa.Filters, filters...)
|
||||
return nil
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// return the filter object that matches an IP address
|
||||
|
||||
func (roa *ROA) MatchFilter(ip net.IP) *ROAFilter {
|
||||
for _, filter := range roa.Filters {
|
||||
if filter.Network.Contains(ip) {
|
||||
return filter
|
||||
}
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"IP": ip,
|
||||
}).Error("Couldn't match address to filter !")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// compile ROA data
|
||||
|
||||
func (roa *ROA) CompileROA(registry *Registry,
|
||||
tname string) []*PrefixROA {
|
||||
|
||||
// prepare indices to the route object keys
|
||||
stype := registry.Schema[tname]
|
||||
routeIX := stype.KeyIndex[tname]
|
||||
originIX := stype.KeyIndex["origin"]
|
||||
mlenIX := stype.KeyIndex["max-length"]
|
||||
|
||||
roalist := make([]*PrefixROA, 0, len(routeIX.Objects))
|
||||
|
||||
// for each object that has a route key
|
||||
for object, rattribs := range routeIX.Objects {
|
||||
|
||||
if len(rattribs) > 1 {
|
||||
log.WithFields(log.Fields{
|
||||
"object": object.Ref,
|
||||
}).Warn("Found object with multiple route attributes")
|
||||
}
|
||||
|
||||
// extract the prefix
|
||||
prefix := rattribs[0].RawValue
|
||||
_, pnet, err := net.ParseCIDR(prefix)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"object": object.Ref,
|
||||
"prefix": prefix,
|
||||
"error": err,
|
||||
}).Error("Unable to parse CIDR in ROA")
|
||||
continue
|
||||
}
|
||||
|
||||
// match the prefix to the prefix filters
|
||||
filter := roa.MatchFilter(pnet.IP)
|
||||
if filter == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// don't allow routes that are denied in the filter rules
|
||||
if filter.Action == "deny" {
|
||||
log.WithFields(log.Fields{
|
||||
"object": object.Ref,
|
||||
"prefix": prefix,
|
||||
"filter": filter.Prefix,
|
||||
}).Warn("Denied ROA through filter rule")
|
||||
continue
|
||||
}
|
||||
|
||||
mlen := filter.MaxLen
|
||||
|
||||
// if the prefix is greater than the filter.MaxLen
|
||||
// then don't emit an ROA route (making the route invalid)
|
||||
if ones, _ := pnet.Mask.Size(); ones > int(mlen) {
|
||||
log.WithFields(log.Fields{
|
||||
"object": object.Ref,
|
||||
"prefix": prefix,
|
||||
"filter": filter.Prefix,
|
||||
}).Debug("Defined ROA: Prefix > filter MaxLen")
|
||||
continue
|
||||
}
|
||||
|
||||
// calculate the max-length for this object
|
||||
|
||||
// check if the attribute has max-length defined
|
||||
mattrib := mlenIX.Objects[object]
|
||||
if mattrib != nil {
|
||||
|
||||
// use the local max-length value
|
||||
tmp, err := strconv.ParseUint(mattrib[0].RawValue, 10, 8)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"object": object.Ref,
|
||||
"max-length": mattrib[0].RawValue,
|
||||
"error": err,
|
||||
}).Warn("Unable to convert max-length attribute")
|
||||
} else {
|
||||
|
||||
// filter rules still have precedence over local values
|
||||
if (uint8(tmp) < mlen) && (uint8(tmp) > filter.MinLen) {
|
||||
mlen = uint8(tmp)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// look up the origin key for this object
|
||||
oattribs := originIX.Objects[object]
|
||||
if oattribs == nil {
|
||||
log.WithFields(log.Fields{
|
||||
"object": object.Ref,
|
||||
}).Warn("Route Object without Origin")
|
||||
} else {
|
||||
|
||||
// then for origin that can announce this prefix
|
||||
for _, oattrib := range oattribs {
|
||||
|
||||
// add the ROA
|
||||
roalist = append(roalist, &PrefixROA{
|
||||
Prefix: prefix,
|
||||
MaxLen: mlen,
|
||||
ASN: oattrib.RawValue,
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return roalist
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
55
static.go
Normal file
55
static.go
Normal file
@ -0,0 +1,55 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Registry API Server
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// called from main to initialise the API routing
|
||||
|
||||
func InstallStaticRoutes(router *mux.Router, staticPath string) {
|
||||
|
||||
// an empty path disables static route serving
|
||||
if staticPath == "" {
|
||||
log.Info("Disabling static route serving")
|
||||
return
|
||||
}
|
||||
|
||||
// validate that the staticPath exists
|
||||
stat, err := os.Stat(staticPath)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": staticPath,
|
||||
}).Fatal("Unable to find static page directory")
|
||||
}
|
||||
|
||||
// and it is a directory
|
||||
if !stat.IsDir() {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": staticPath,
|
||||
}).Fatal("Static path is not a directory")
|
||||
}
|
||||
|
||||
// install a file server for the static route
|
||||
router.PathPrefix("/").Handler(http.StripPrefix("/",
|
||||
http.FileServer(http.Dir(staticPath)))).Methods("GET")
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"path": staticPath,
|
||||
}).Info("Static route installed")
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
Loading…
x
Reference in New Issue
Block a user