commit
f91551b74d
120
dyndns/handler/cname.go
Normal file
120
dyndns/handler/cname.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/benjaminbear/docker-ddns-server/dyndns/model"
|
||||
"github.com/benjaminbear/docker-ddns-server/dyndns/nswrapper"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// ListCNames fetches all cnames from database and lists them on the website.
|
||||
func (h *Handler) ListCNames(c echo.Context) (err error) {
|
||||
if !h.AuthAdmin {
|
||||
return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED})
|
||||
}
|
||||
|
||||
cnames := new([]model.CName)
|
||||
if err = h.DB.Preload("Target").Find(cnames).Error; err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "listcnames", echo.Map{
|
||||
"cnames": cnames,
|
||||
})
|
||||
}
|
||||
|
||||
// AddCName just renders the "add cname" website.
|
||||
// Therefore all host entries from the database are being fetched.
|
||||
func (h *Handler) AddCName(c echo.Context) (err error) {
|
||||
if !h.AuthAdmin {
|
||||
return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED})
|
||||
}
|
||||
|
||||
hosts := new([]model.Host)
|
||||
if err = h.DB.Find(hosts).Error; err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "addcname", echo.Map{
|
||||
"config": h.Config,
|
||||
"hosts": hosts,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateCName validates the cname data from the "add cname" website,
|
||||
// adds the cname entry to the database,
|
||||
// and adds the entry to the DNS server.
|
||||
func (h *Handler) CreateCName(c echo.Context) (err error) {
|
||||
if !h.AuthAdmin {
|
||||
return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED})
|
||||
}
|
||||
|
||||
cname := &model.CName{}
|
||||
if err = c.Bind(cname); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
host := &model.Host{}
|
||||
if err = h.DB.First(host, c.FormValue("target_id")).Error; err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
cname.Target = *host
|
||||
|
||||
if err = c.Validate(cname); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
if err = h.checkUniqueHostname(cname.Hostname, cname.Target.Domain); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
if err = h.DB.Create(cname).Error; err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
if err = nswrapper.UpdateRecord(cname.Hostname, fmt.Sprintf("%s.%s", cname.Target.Hostname, cname.Target.Domain), "CNAME", cname.Target.Domain, cname.Ttl); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, cname)
|
||||
}
|
||||
|
||||
// DeleteCName fetches a cname entry from the database by "id"
|
||||
// and deletes the database and DNS server entry to it.
|
||||
func (h *Handler) DeleteCName(c echo.Context) (err error) {
|
||||
if !h.AuthAdmin {
|
||||
return c.JSON(http.StatusUnauthorized, &Error{UNAUTHORIZED})
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
cname := &model.CName{}
|
||||
if err = h.DB.First(cname, id).Error; err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
err = h.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err = tx.Unscoped().Delete(cname).Error; err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
if err = nswrapper.DeleteRecord(cname.Hostname, cname.Target.Domain); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, id)
|
||||
}
|
|
@ -116,6 +116,10 @@ func (h *Handler) InitDB() (err error) {
|
|||
h.DB.CreateTable(&model.Host{})
|
||||
}
|
||||
|
||||
if !h.DB.HasTable(&model.CName{}) {
|
||||
h.DB.CreateTable(&model.CName{})
|
||||
}
|
||||
|
||||
if !h.DB.HasTable(&model.Log{}) {
|
||||
h.DB.CreateTable(&model.Log{})
|
||||
}
|
||||
|
|
|
@ -106,6 +106,10 @@ func (h *Handler) CreateHost(c echo.Context) (err error) {
|
|||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
if err = h.checkUniqueHostname(host.Hostname, host.Domain); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
if err = h.DB.Create(host).Error; err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
@ -198,6 +202,10 @@ func (h *Handler) DeleteHost(c echo.Context) (err error) {
|
|||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
if err = tx.Where(&model.CName{TargetID: uint(id)}).Delete(&model.CName{}).Error; err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &Error{err.Error()})
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -282,3 +290,28 @@ func (h *Handler) UpdateIP(c echo.Context) (err error) {
|
|||
|
||||
return c.String(http.StatusOK, "good\n")
|
||||
}
|
||||
|
||||
func (h *Handler) checkUniqueHostname(hostname, domain string) error {
|
||||
fmt.Println(hostname, domain)
|
||||
hosts := new([]model.Host)
|
||||
if err := h.DB.Where(&model.Host{Hostname: hostname, Domain: domain}).Find(hosts).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(*hosts) > 0 {
|
||||
return fmt.Errorf("hostname already exists")
|
||||
}
|
||||
|
||||
cnames := new([]model.CName)
|
||||
if err := h.DB.Preload("Target").Where(&model.CName{Hostname: hostname}).Find(cnames).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cname := range *cnames {
|
||||
if cname.Target.Domain == domain {
|
||||
return fmt.Errorf("hostname already exists")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"github.com/benjaminbear/docker-ddns-server/dyndns/handler"
|
||||
"github.com/foolin/goview/supports/echoview-v4"
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
|
@ -53,6 +52,8 @@ func main() {
|
|||
e.GET("/hosts/add", h.AddHost)
|
||||
e.GET("/hosts/edit/:id", h.EditHost)
|
||||
e.GET("/hosts", h.ListHosts)
|
||||
e.GET("/cnames/add", h.AddCName)
|
||||
e.GET("/cnames", h.ListCNames)
|
||||
e.GET("/logs", h.ShowLogs)
|
||||
e.GET("/logs/host/:id", h.ShowHostLogs)
|
||||
|
||||
|
@ -60,6 +61,8 @@ func main() {
|
|||
e.POST("/hosts/add", h.CreateHost)
|
||||
e.POST("/hosts/edit/:id", h.UpdateHost)
|
||||
e.GET("/hosts/delete/:id", h.DeleteHost)
|
||||
e.POST("/cnames/add", h.CreateCName)
|
||||
e.GET("/cnames/delete/:id", h.DeleteCName)
|
||||
|
||||
// dyndns compatible api
|
||||
e.GET("/update", h.UpdateIP)
|
||||
|
|
14
dyndns/model/cname.go
Normal file
14
dyndns/model/cname.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// CName is a dns cname entry.
|
||||
type CName struct {
|
||||
gorm.Model
|
||||
Hostname string `gorm:"not null" form:"hostname" validate:"required,hostname"`
|
||||
Target Host `validate:"required,hostname"`
|
||||
TargetID uint
|
||||
Ttl int `form:"ttl" validate:"required,min=20,max=86400"`
|
||||
}
|
|
@ -10,8 +10,8 @@ import (
|
|||
)
|
||||
|
||||
// UpdateRecord builds a nsupdate file and updates a record by executing it with nsupdate.
|
||||
func UpdateRecord(hostname string, ipAddr string, addrType string, zone string, ttl int) error {
|
||||
fmt.Printf("%s record update request: %s -> %s\n", addrType, hostname, ipAddr)
|
||||
func UpdateRecord(hostname string, target string, addrType string, zone string, ttl int) error {
|
||||
fmt.Printf("%s record update request: %s -> %s\n", addrType, hostname, target)
|
||||
|
||||
f, err := ioutil.TempFile(os.TempDir(), "dyndns")
|
||||
if err != nil {
|
||||
|
@ -24,7 +24,7 @@ func UpdateRecord(hostname string, ipAddr string, addrType string, zone string,
|
|||
w.WriteString(fmt.Sprintf("server %s\n", "localhost"))
|
||||
w.WriteString(fmt.Sprintf("zone %s\n", zone))
|
||||
w.WriteString(fmt.Sprintf("update delete %s.%s %s\n", hostname, zone, addrType))
|
||||
w.WriteString(fmt.Sprintf("update add %s.%s %v %s %s\n", hostname, zone, ttl, addrType, ipAddr))
|
||||
w.WriteString(fmt.Sprintf("update add %s.%s %v %s %s\n", hostname, zone, ttl, addrType, target))
|
||||
w.WriteString("send\n")
|
||||
|
||||
w.Flush()
|
||||
|
|
|
@ -38,15 +38,24 @@ $("button.add, button.edit").click(function () {
|
|||
action = "edit";
|
||||
}
|
||||
|
||||
let type;
|
||||
if ($(this).hasClass("host")) {
|
||||
type = "hosts";
|
||||
}
|
||||
|
||||
if ($(this).hasClass("cname")) {
|
||||
type = "cnames";
|
||||
}
|
||||
|
||||
$('#domain').prop('disabled', false);
|
||||
|
||||
$.ajax({
|
||||
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
data: $('#editHostForm').serialize(),
|
||||
type: 'POST',
|
||||
url: '/hosts/'+action+id,
|
||||
url: '/'+type+'/'+action+id,
|
||||
}).done(function(data, textStatus, jqXHR) {
|
||||
location.href="/hosts";
|
||||
location.href="/"+type;
|
||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||
alert("Error: " + $.parseJSON(jqXHR.responseText).message);
|
||||
});
|
||||
|
@ -79,6 +88,29 @@ $("#logout").click(function (){
|
|||
}
|
||||
});
|
||||
|
||||
$("button.addCName").click(function () {
|
||||
location.href='/cnames/add';
|
||||
});
|
||||
|
||||
$("button.deleteCName").click(function () {
|
||||
$.ajax({
|
||||
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
type: 'GET',
|
||||
url: "/cnames/delete/" + $(this).attr('id')
|
||||
}).done(function(data, textStatus, jqXHR) {
|
||||
location.href="/cnames";
|
||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
||||
alert("Error: " + $.parseJSON(jqXHR.responseText).message);
|
||||
location.reload()
|
||||
});
|
||||
});
|
||||
|
||||
function newTargetSelected() {
|
||||
var sel = document.getElementById("target_id");
|
||||
var x = sel.options[sel.selectedIndex].label.replace(sel.options[sel.selectedIndex].text, '');
|
||||
document.getElementById("domain_mirror").value = x;
|
||||
}
|
||||
|
||||
$("button.copyToClipboard").click(function () {
|
||||
let id;
|
||||
if ($(this).hasClass('username')) {
|
||||
|
@ -122,4 +154,10 @@ $(document).ready(function(){
|
|||
return $(this).prop('title');
|
||||
}
|
||||
});
|
||||
|
||||
urlPath = new URL(window.location.href).pathname.split("/")[1];
|
||||
if (urlPath === "") {
|
||||
urlPath = "hosts"
|
||||
}
|
||||
document.getElementsByClassName("nav-"+urlPath)[0].classList.add("active");
|
||||
});
|
48
dyndns/views/addcname.html
Normal file
48
dyndns/views/addcname.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
{{define "content"}}
|
||||
<div class="p-4" style="background-color: #e9ecef">
|
||||
<h3 class="text-center mb-4">Add CName Entry</h3>
|
||||
<form id="editHostForm" action="javascript:void(0);">
|
||||
<div class="row mt-3">
|
||||
<div class="col-1"></div>
|
||||
<div class="col-2 text-right">Hostname:</div>
|
||||
<div class="col-8 input-group">
|
||||
<input type="text" class="form-control" placeholder="Enter hostname" name="hostname">
|
||||
<div class="input-group-append">
|
||||
<input type="text" class="form-control" placeholder="Select target.." id="domain_mirror" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-1"></div>
|
||||
<div class="col-2 text-right">Target:</div>
|
||||
<div class="col-8">
|
||||
<select class="custom-select" name="target_id" id="target_id" onchange="newTargetSelected()">
|
||||
<option selected>Choose...</option>
|
||||
{{range $host := .hosts}}
|
||||
<a class="dropdown-item"><option label="{{$host.Hostname}}.{{$host.Domain}}" value="{{$host.ID}}">{{$host.Hostname}}</option></a>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-1"></div>
|
||||
<div class="col-2 text-right">TTL:</div>
|
||||
<div class="col-8">
|
||||
<select class="form-control" name="ttl">
|
||||
<option value="20" {{if .cname.Ttl }}{{if eq .cname.Ttl 20 }}selected{{end}}{{end}}>20 s. Super dynamic DNS for frequent updates</option>
|
||||
<option value="60" {{if .cname.Ttl }}{{if eq .cname.Ttl 60 }}selected{{end}}{{end}}>60 s. Default dynamic DNS value</option>
|
||||
<option value="3600" {{if .cname.Ttl }}{{if eq .cname.Ttl 3600 }}selected{{end}}{{end}}>1 hr. Rarely updated IP address</option>
|
||||
<option value="14400" {{if .cname.Ttl }}{{if eq .cname.Ttl 14400 }}selected{{end}}{{end}}>4 hrs. Static record with benefits of DNS caching</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-11 d-flex justify-content-end"><button id="{{.cname.ID}}" class="add cname btn btn-primary">Add CName Entry</button></div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
|
@ -68,7 +68,7 @@
|
|||
<div class="col-1"></div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-11 d-flex justify-content-end"><button id="{{.host.ID}}" class="{{.addEdit}} btn btn-primary">{{if eq .addEdit "edit" }}Edit{{else if eq .addEdit "add" }}Add{{end}} Host Entry</button></div>
|
||||
<div class="col-11 d-flex justify-content-end"><button id="{{.host.ID}}" class="{{.addEdit}} host btn btn-primary">{{if eq .addEdit "edit" }}Edit{{else if eq .addEdit "add" }}Add{{end}} Host Entry</button></div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -27,13 +27,16 @@
|
|||
<nav>
|
||||
<ul class="nav nav-pills float-right">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/hosts">Hosts <span class="sr-only">(current)</span></a>
|
||||
<a class="nav-link nav-hosts" href="/hosts">Hosts</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/logs">Logs</a>
|
||||
<a class="nav-link nav-cnames" href="/cnames">CNames</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="" id="logout">Logout</a>
|
||||
<a class="nav-link nav-logs" href="/logs">Logs</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link nav-logout" href="" id="logout">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
@ -44,7 +47,7 @@
|
|||
{{template "content" .}}
|
||||
|
||||
<footer class="footer">
|
||||
<p>© TheBBCloud 2020</p>
|
||||
<p>© TheBBCloud 2021</p>
|
||||
</footer>
|
||||
|
||||
</div> <!-- /container -->
|
||||
|
@ -57,6 +60,6 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU=" crossorigin="anonymous"></script>
|
||||
<script src="/static/js/actions.js"></script>
|
||||
<script src="/static/js/actions-1.0.0.js"></script>
|
||||
</body>
|
||||
</html>
|
25
dyndns/views/listcnames.html
Normal file
25
dyndns/views/listcnames.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
{{define "content"}}
|
||||
<div class="container marketing">
|
||||
<h3 class="text-center mb-4">DNS CName Entries</h3>
|
||||
<table class="table table-striped text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>Target</th>
|
||||
<th>TTL</th>
|
||||
<th><button class="addCName btn btn-primary">Add CName Entry</button></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .cnames}}
|
||||
<tr>
|
||||
<td>{{.Hostname}}.{{.Target.Domain}}</td>
|
||||
<td>{{.Target.Hostname}}.{{.Target.Domain}}</td>
|
||||
<td>{{.Ttl}}</td>
|
||||
<td><button id="{{.ID}}" class="deleteCName btn btn-outline-secondary btn-sm"><img src="/static/icons/trash.svg" alt="" width="16" height="16" title="Delete"></button></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
Loading…
Reference in a new issue