Document code

This commit is contained in:
nemunaire 2020-07-28 17:34:55 +02:00
parent 653821262c
commit 9a6e0504a9
20 changed files with 539 additions and 79 deletions

148
README.md Normal file
View File

@ -0,0 +1,148 @@
happyDNS
========
Finally a simple, modern and open source interface for domain name.
It consists of a HTTP REST API written in Golang (primarily based on https://github.com/miekg/dns) with a nice web interface written in Vue.js.
It runs as a single stateless Linux binary, backed by a database (currently: LevelDB, more SGBD to come soon).
Features
--------
TODO
Building
--------
### Dependancies
In order to build the happyDNS project, you'll need the following dependancies:
* `go` at least version 1.13
* `go-bindata`
* `nodejs` tested with version 14.4.0
* `yarn` tested with version 1.22.4
### Instructions
1. First, I'll need to prepare the frontend.
Go inside the `htdocs/` directory and install the node modules dependancies:
```
cd htdocs/
yarn install
```
2. Generates assets files used by Go code:
```
cd .. # Go back to the root of the project
go generate
```
3. Build the Go code:
```
go build
```
The command will create a binary `happydns` you can use standalone.
Install at home
---------------
The binary comes with sane default options to start with.
You can simply launch the following command in your terminal:
```
./happydns
```
After some initialization, it should show you:
Admin listening on ./happydns.sock
Ready, listening on :8081
Go to http://localhost:8081/ to start using happyDNS.
### Database configuration
By default, the LevelDB storage engine is used. You can change the storage engine using the option `-storage-engine other-engine`.
The help command `./happydns -help` can show you the available engines. By example:
-storage-engine value
Select the storage engine between [leveldb mysql] (default leveldb)
#### LevelDB
-leveldb-path string
Path to the LevelDB Database (default "happydns.db")
By default, a new directory is created near the binary, called `happydns.db`. This directory contains the database used by the program. You can change it to a more
### Persistant configuration
The binary will automatically look for some existing configuration files:
* `./happydns.conf` in the current directory;
* `$XDG_CONFIG_HOME/happydns/happydns.conf`;
* `/etc/happydns.conf`.
Only the first file found will be used.
It is also possible to specify a custom path by adding it as argument to the command line:
```
./happydns /etc/happydns/config
```
#### Config file format
Comments line has to begin with #, it is not possible to have comments at the end of a line, by appending # followed by a comment.
Place on each line the name of the config option and the expected value, separated by `=`. For example:
```
storage-engine=leveldb
leveldb-path=/var/lib/happydns/db/
```
#### Environment variables
It'll also look for special environment variables, beginning with `HAPPYDNS_`.
You can achieve the same as the previous example, with the following environment variables:
```
HAPPYDNS_STORAGE_ENGINE=leveldb
HAPPYDNS_LEVELDB_PATH=/var/lib/happydns/db/
```
You just have to replace dash by underscore.
Development environment
-----------------------
If you want to contribute to the frontend, instead of regenerating the frontend assets each time you made a modification (with `go generate`), you can use the development tools:
In one terminal, run `happydns` with the following arguments:
```
./happydns -dev http://127.0.0.1:8080
```
In another terminal, run the node part:
```
cd htdocs/
yarn run serve
```
With this setup, static assets integrated inside the go binary will not be used, instead it'll forward all request for static assets to the node server, that do dynamic reload, etc.

View File

@ -38,6 +38,7 @@ import (
"git.happydns.org/happydns/storage"
)
// declareFlags registers flags for the structure Options.
func (o *Options) declareFlags() {
flag.StringVar(&o.DevProxy, "dev", o.DevProxy, "Proxify traffic to this host for static assets")
flag.StringVar(&o.AdminBind, "admin-bind", o.AdminBind, "Bind port/socket for administration interface")
@ -50,6 +51,7 @@ func (o *Options) declareFlags() {
// Others flags are declared in some other files likes sources, storages, ... when they need specials configurations
}
// parseCLI parse the flags and treats extra args as configuration filename.
func (o *Options) parseCLI() error {
flag.Parse()

View File

@ -42,25 +42,47 @@ import (
"git.happydns.org/happydns/storage"
)
// Options stores the configuration of the software.
type Options struct {
Bind string
AdminBind string
ExternalURL string
BaseURL string
DevProxy string
// Bind is the address:port used to bind the main interface with API.
Bind string
// AdminBind is the address:port or unix socket used to serve the admin
// API.
AdminBind string
// ExternalURL keeps the URL used in communications (such as email,
// ...), when it needs to use complete URL, not only relative parts.
ExternalURL string
// BaseURL is the relative path where begins the root of the app.
BaseURL string
// DevProxy is the URL that override static assets.
DevProxy string
// DefaultNameServer is the NS server suggested by default.
DefaultNameServer string
StorageEngine storage.StorageEngine
// StorageEngine points to the storage engine used.
StorageEngine storage.StorageEngine
}
// BuildURL appends the given url to the absolute ExternalURL.
func (o *Options) BuildURL(url string) string {
return fmt.Sprintf("%s%s%s", o.ExternalURL, o.BaseURL, url)
}
// BuildURL_noescape build an URL containing formater.
func (o *Options) BuildURL_noescape(url string, args ...interface{}) string {
args = append([]interface{}{o.ExternalURL, o.BaseURL}, args...)
return fmt.Sprintf("%s%s"+url, args...)
}
// ConsolidateConfig fills an Options struct by reading configuration from
// config files, environment, then command line.
//
// Should be called only one time.
func ConsolidateConfig() (opts *Options, err error) {
// Define defaults options
opts = &Options{
@ -119,6 +141,8 @@ func ConsolidateConfig() (opts *Options, err error) {
return
}
// parseLine treats a config line and place the read value in the variable
// declared to the corresponding flag.
func (o *Options) parseLine(line string) (err error) {
fields := strings.SplitN(line, "=", 2)
orig_key := strings.TrimSpace(fields[0])

View File

@ -37,6 +37,8 @@ import (
"strings"
)
// parseEnvironmentVariables analyzes all the environment variables to find
// each one starting by HAPPYDNS_
func (o *Options) parseEnvironmentVariables() (err error) {
for _, line := range os.Environ() {
if strings.HasPrefix(line, "HAPPYDNS_") {

View File

@ -38,6 +38,8 @@ import (
"strings"
)
// parseFile opens the file at the given filename path, then treat each line
// not starting with '#' as a configuration statement.
func (o *Options) parseFile(filename string) error {
fp, err := os.Open(filename)
if err != nil {

View File

@ -37,10 +37,14 @@ import (
"git.happydns.org/happydns/model"
)
// GetAccountRecoveryURL returns the absolute URL corresponding to the recovery
// URL of the given account.
func (o *Options) GetAccountRecoveryURL(u *happydns.User) string {
return o.BuildURL_noescape("/forgotten-password?u=%x&k=%s", u.Id, url.QueryEscape(u.GenAccountRecoveryHash(false)))
}
// GetRegistrationURL returns the absolute URL corresponding to the e-mail
// validation page of the given account.
func (o *Options) GetRegistrationURL(u *happydns.User) string {
return o.BuildURL_noescape("/email-validation?u=%x&k=%s", u.Id, url.QueryEscape(u.GenRegistrationHash(false)))
}

View File

@ -32,27 +32,34 @@
package happydns
import (
"strings"
"github.com/miekg/dns"
)
// Domain holds information about a domain name own by a User.
type Domain struct {
Id int64 `json:"id"`
IdUser int64 `json:"id_owner"`
IdSource int64 `json:"id_source"`
DomainName string `json:"domain"`
// Id is the Domain's identifier in the database.
Id int64 `json:"id"`
// IdUser is the identifier of the Domain's Owner.
IdUser int64 `json:"id_owner"`
// IsSource is the identifier of the Source used to access and edit the
// Domain.
IdSource int64 `json:"id_source"`
// DomainName is the FQDN of the managed Domain.
DomainName string `json:"domain"`
// ZoneHistory are the identifiers to the Zone attached to the current
// Domain.
ZoneHistory []int64 `json:"zone_history"`
}
// Domains is an array of Domain.
type Domains []*Domain
func (d *Domain) NormalizedNSServer() string {
if strings.Index(d.DomainName, ":") > -1 {
return d.DomainName
} else {
return d.DomainName + ":53"
}
}
// HasZone checks if the given Zone's identifier is part of this Domain
// history.
func (d *Domain) HasZone(zoneId int64) (found bool) {
for _, v := range d.ZoneHistory {
if v == zoneId {
@ -62,14 +69,13 @@ func (d *Domain) HasZone(zoneId int64) (found bool) {
return
}
// NewDomain fills a new Domain structure.
func NewDomain(u *User, st *SourceMeta, dn string) (d *Domain) {
d = &Domain{
IdUser: u.Id,
IdSource: st.Id,
DomainName: dn,
DomainName: dns.Fqdn(dn),
}
d.DomainName = d.NormalizedNSServer()
return
}

View File

@ -37,31 +37,59 @@ import (
// Service represents a service provided by one or more DNS record.
type Service interface {
// GetNbResources get the number of main Resources contains in the Service.
GetNbResources() int
// GenComment sum up the content of the Service, in a small usefull string.
GenComment(origin string) string
// genRRs generates corresponding RRs.
GenRRs(domain string, ttl uint32, origin string) []dns.RR
}
type ServiceType struct {
Type string `json:"_svctype"`
Id []byte `json:"_id,omitempty"`
OwnerId int64 `json:"_ownerid,omitempty"`
Domain string `json:"_domain"`
Ttl uint32 `json:"_ttl"`
Comment string `json:"_comment,omitempty"`
UserComment string `json:"_mycomment,omitempty"`
Aliases []string `json:"_aliases,omitempty"`
NbResources int `json:"_tmp_hint_nb"`
// ServiceMeta holds the metadata associated to a Service.
type ServiceMeta struct {
// Type is the string representation of the Service's type.
Type string `json:"_svctype"`
// Id is the Service's identifier.
Id []byte `json:"_id,omitempty"`
// OwnerId is the User's identifier for the current Service.
OwnerId int64 `json:"_ownerid,omitempty"`
// Domain contains the abstract domain where this Service relates.
Domain string `json:"_domain"`
// Ttl contains the specific TTL for the underlying Resources.
Ttl uint32 `json:"_ttl"`
// Comment is a string that helps user to distinguish the Service.
Comment string `json:"_comment,omitempty"`
// UserComment is a supplementary string defined by the user to
// distinguish the Service.
UserComment string `json:"_mycomment,omitempty"`
// Aliases exposes the aliases defined on this Service.
Aliases []string `json:"_aliases,omitempty"`
// NbResources holds the number of Resources stored inside this Service.
NbResources int `json:"_tmp_hint_nb"`
}
// ServiceCombined combined ServiceMeta + Service
type ServiceCombined struct {
Service
ServiceType
ServiceMeta
}
// UnmarshalServiceJSON stores a functor defined in services/interfaces.go that
// can't be defined here due to cyclic imports.
var UnmarshalServiceJSON func(*ServiceCombined, []byte) error
// UnmarshalJSON points to the implementation of the UnmarshalJSON function for
// the encoding/json module.
func (svc *ServiceCombined) UnmarshalJSON(b []byte) error {
return UnmarshalServiceJSON(svc, b)
}

View File

@ -39,14 +39,25 @@ import (
"time"
)
// Session holds informatin about a User's currently connected.
type Session struct {
Id []byte `json:"id"`
IdUser int64 `json:"login"`
Time time.Time `json:"time"`
// Id is the Session's identifier.
Id []byte `json:"id"`
// IdUser is the User's identifier of the Session.
IdUser int64 `json:"login"`
// Time holds the creation date of the Session.
Time time.Time `json:"time"`
// Content stores data filled by other modules.
Content map[string][]byte `json:"content,omitempty"`
// changed indicates if Content has changed since its loading.
changed bool
}
// NewSession fills a new Session structure.
func NewSession(user *User) (s *Session, err error) {
session_id := make([]byte, 255)
_, err = rand.Read(session_id)
@ -61,10 +72,13 @@ func NewSession(user *User) (s *Session, err error) {
return
}
// HasChanged tells if the Session has changed since its last loading.
func (s *Session) HasChanged() bool {
return s.changed
}
// FindNewKey returns a key and an identifier appended to the given
// prefix, that is available in the User's Session.
func (s *Session) FindNewKey(prefix string) (key string, id int64) {
for {
// max random id is 2^53 to fit on float64 without loosing precision (JSON limitation)
@ -78,6 +92,9 @@ func (s *Session) FindNewKey(prefix string) (key string, id int64) {
return
}
// SetValue defines, erase or delete a content to stores at the given
// key. If the key is already defined, it erases its content. If the
// given value is nil, it deletes the key.
func (s *Session) SetValue(key string, value interface{}) {
if s.Content == nil && value != nil {
s.Content = map[string][]byte{}
@ -98,6 +115,8 @@ func (s *Session) SetValue(key string, value interface{}) {
}
}
// GetValue retrieves data stored at the given key. Returns true if
// the key exists and if the value has been filled correctly.
func (s *Session) GetValue(key string, value interface{}) bool {
if v, ok := s.Content[key]; !ok {
return false
@ -108,10 +127,12 @@ func (s *Session) GetValue(key string, value interface{}) bool {
}
}
// DropKey removes the given key from the Session's Content.
func (s *Session) DropKey(key string) {
s.SetValue(key, nil)
}
// ClearSession removes all content from the Session.
func (s *Session) ClearSession() {
s.Content = nil
s.changed = true

View File

@ -35,22 +35,44 @@ import (
"github.com/miekg/dns"
)
// Source is where Domains and Zones can be managed.
type Source interface {
// Validate tells if the Source's settings are good.
Validate() error
// DomainExists tells if the given domain exists for the Source.
DomainExists(string) error
// ImportZone retrieves all RRs for the given Domain.
ImportZone(*Domain) ([]dns.RR, error)
// AddRR adds an RR in the zone of the given Domain.
AddRR(*Domain, dns.RR) error
// DeleteRR removes the given RR in the zone of the given Domain.
DeleteRR(*Domain, dns.RR) error
// UpdateSOA tries to update the Zone's SOA record, according to the
// given parameters.
UpdateSOA(*Domain, *dns.SOA, bool) error
}
// SourceMeta holds the metadata associated to a Source.
type SourceMeta struct {
Type string `json:"_srctype"`
Id int64 `json:"_id"`
OwnerId int64 `json:"_ownerid"`
// Type is the string representation of the Source's type.
Type string `json:"_srctype"`
// Id is the Source's identifier.
Id int64 `json:"_id"`
// OwnerId is the User's identifier for the current Source.
OwnerId int64 `json:"_ownerid"`
// Comment is a string that helps user to distinguish the Source.
Comment string `json:"_comment,omitempty"`
}
// SourceCombined combined SourceMeta + Source
type SourceCombined struct {
Source
SourceMeta

View File

@ -43,17 +43,31 @@ import (
"golang.org/x/crypto/bcrypt"
)
// User represents an account.
type User struct {
Id int64
Email string
Password []byte
RegistrationTime *time.Time
EmailValidated *time.Time
// Id is the User's identifier.
Id int64
// Email is the User's login and mean of contact.
Email string
// Password is hashed
Password []byte
// RegistrationTime is the time when the User has register is account.
RegistrationTime *time.Time
// EmailValidated is the time when the User validate its email address.
EmailValidated *time.Time
// PasswordRecoveryKey is a string generated when User asks to recover its account.
PasswordRecoveryKey []byte `json:",omitempty"`
}
// Users is a group of User.
type Users []*User
// NewUser fills a new User structure.
func NewUser(email string, password string) (u *User, err error) {
t := time.Now()
@ -68,6 +82,7 @@ func NewUser(email string, password string) (u *User, err error) {
return
}
// CheckPasswordConstraints checks the given password is strong enough.
func (u *User) CheckPasswordConstraints(password string) (err error) {
if len(password) < 8 {
return fmt.Errorf("Password has to be at least 8 characters long.")
@ -86,6 +101,7 @@ func (u *User) CheckPasswordConstraints(password string) (err error) {
return nil
}
// DefinePassword erases the current User's password by the new one given.
func (u *User) DefinePassword(password string) (err error) {
if err = u.CheckPasswordConstraints(password); err != nil {
return
@ -97,12 +113,16 @@ func (u *User) DefinePassword(password string) (err error) {
return
}
// CheckAuth compares the given password to the hashed one in the User struct.
func (u *User) CheckAuth(password string) bool {
return bcrypt.CompareHashAndPassword(u.Password, []byte(password)) == nil
}
// RegistrationHashValidity is the time during which the email validation link is at least valid.
const RegistrationHashValidity = 24 * time.Hour
// GenRegistrationHash generates the validation hash for the current or previous period.
// The hash computation is based on some already filled fields in the structure.
func (u *User) GenRegistrationHash(previous bool) string {
date := time.Now()
if previous {
@ -118,6 +138,7 @@ func (u *User) GenRegistrationHash(previous bool) string {
)
}
// ValidateEmail tries to validate the email address by comparing the given key to the expected one.
func (u *User) ValidateEmail(key string) error {
if key == u.GenRegistrationHash(false) || key == u.GenRegistrationHash(true) {
now := time.Now()
@ -128,8 +149,12 @@ func (u *User) ValidateEmail(key string) error {
return fmt.Errorf("The validation address link you follow is invalid or has expired (it is valid during %d hours)", RegistrationHashValidity/time.Hour)
}
// AccountRecoveryHashValidityis the time during which the recovery link is at least valid.
const AccountRecoveryHashValidity = 2 * time.Hour
// GenAccountRecoveryHash generates the recovery hash for the current or previous period.
// It updates the User structure in some cases, when it needs to generate a new recovery key,
// so don't forget to save the changes made.
func (u *User) GenAccountRecoveryHash(previous bool) string {
if u.PasswordRecoveryKey == nil {
u.PasswordRecoveryKey = make([]byte, 64)
@ -155,6 +180,7 @@ func (u *User) GenAccountRecoveryHash(previous bool) string {
)
}
// CanRecoverAccount checks if the given key is a valid recovery hash.
func (u *User) CanRecoverAccount(key string) error {
if key == u.GenAccountRecoveryHash(false) || key == u.GenAccountRecoveryHash(true) {
return nil

View File

@ -39,21 +39,37 @@ import (
"github.com/miekg/dns"
)
// ZoneMeta holds the metadata associated to a Zone.
type ZoneMeta struct {
Id int64 `json:"id"`
IdAuthor int64 `json:"id_author"`
DefaultTTL uint32 `json:"default_ttl"`
LastModified time.Time `json:"last_modified,omitempty"`
CommitMsg *string `json:"commit_message,omitempty"`
CommitDate *time.Time `json:"commit_date,omitempty"`
Published *time.Time `json:"published,omitempty"`
// Id is the Zone's identifier.
Id int64 `json:"id"`
// IdAuthor is the User's identifier for the current Zone.
IdAuthor int64 `json:"id_author"`
// DefaultTTL is the TTL to use when no TTL has been defined for a record in this Zone.
DefaultTTL uint32 `json:"default_ttl"`
// LastModified holds the time when the last modification has been made on this Zone.
LastModified time.Time `json:"last_modified,omitempty"`
// CommitMsg is a message defined by the User to give a label to this Zone revision.
CommitMsg *string `json:"commit_message,omitempty"`
// CommitDate is the time when the commit has been made.
CommitDate *time.Time `json:"commit_date,omitempty"`
// Published indicates whether the Zone has already been published or not.
Published *time.Time `json:"published,omitempty"`
}
// Zone contains ZoneMeta + map of services by subdomains.
type Zone struct {
ZoneMeta
Services map[string][]*ServiceCombined `json:"services"`
}
// DerivateNew creates a new Zone from the current one, by copying all fields.
func (z *Zone) DerivateNew() *Zone {
newZone := new(Zone)
@ -69,6 +85,7 @@ func (z *Zone) DerivateNew() *Zone {
return newZone
}
// FindService finds the Service identified by the given id.
func (z *Zone) FindService(id []byte) (string, *ServiceCombined) {
for subdomain := range z.Services {
if svc := z.FindSubdomainService(subdomain, id); svc != nil {
@ -79,6 +96,7 @@ func (z *Zone) FindService(id []byte) (string, *ServiceCombined) {
return "", nil
}
// FindSubdomainService finds the Service identified by the given id, only under the given subdomain.
func (z *Zone) FindSubdomainService(domain string, id []byte) *ServiceCombined {
for _, svc := range z.Services[domain] {
if bytes.Equal(svc.Id, id) {
@ -89,6 +107,8 @@ func (z *Zone) FindSubdomainService(domain string, id []byte) *ServiceCombined {
return nil
}
// EraseService overwrites the Service identified by the given id, under the given subdomain.
// The the new service is nil, it removes the existing Service instead of overwrite it.
func (z *Zone) EraseService(subdomain string, origin string, id []byte, s *ServiceCombined) error {
if services, ok := z.Services[subdomain]; ok {
for k, svc := range services {
@ -116,6 +136,7 @@ func (z *Zone) EraseService(subdomain string, origin string, id []byte, s *Servi
return errors.New("Service not found")
}
// GenerateRRs returns all the reals records of the Zone.
func (z *Zone) GenerateRRs(origin string) (rrs []dns.RR) {
for subdomain, svcs := range z.Services {
if subdomain == "" {

View File

@ -117,7 +117,7 @@ func (a *Analyzer) useRR(rr dns.RR, domain string, svc happydns.Service) error {
ttl = rr.Header().Ttl
}
a.services[domain] = append(a.services[domain], &happydns.ServiceCombined{svc, happydns.ServiceType{
a.services[domain] = append(a.services[domain], &happydns.ServiceCombined{svc, happydns.ServiceMeta{
Id: hash.Sum(nil),
Type: reflect.Indirect(reflect.ValueOf(svc)).Type().String(),
Domain: domain,
@ -181,7 +181,7 @@ func AnalyzeZone(origin string, zone []dns.RR) (svcs map[string][]*happydns.Serv
io.WriteString(hash, record.String())
orphan := &Orphan{record.String()[strings.LastIndex(record.Header().String(), "\tIN\t")+4:]}
svcs[domain] = append(svcs[domain], &happydns.ServiceCombined{orphan, happydns.ServiceType{
svcs[domain] = append(svcs[domain], &happydns.ServiceCombined{orphan, happydns.ServiceMeta{
Id: hash.Sum(nil),
Type: reflect.Indirect(reflect.ValueOf(orphan)).Type().String(),
Domain: domain,

View File

@ -72,8 +72,10 @@ type serviceCombined struct {
Service happydns.Service
}
// UnmarshalServiceJSON implements the UnmarshalJSON function for the
// encoding/json module.
func UnmarshalServiceJSON(svc *happydns.ServiceCombined, b []byte) (err error) {
var svcType happydns.ServiceType
var svcType happydns.ServiceMeta
err = json.Unmarshal(b, &svcType)
if err != nil {
return
@ -89,11 +91,14 @@ func UnmarshalServiceJSON(svc *happydns.ServiceCombined, b []byte) (err error) {
err = json.Unmarshal(b, mySvc)
svc.Service = tsvc
svc.ServiceType = svcType
svc.ServiceMeta = svcType
return
}
func init() {
// Register the UnmarshalServiceJSON variable thats points to the
// Service's UnmarshalJSON implementation that can't be made in model
// module due to cyclic dependancy.
happydns.UnmarshalServiceJSON = UnmarshalServiceJSON
}

View File

@ -39,15 +39,19 @@ import (
"git.happydns.org/happydns/model"
)
// SourceCreator abstract the instanciation of a Source
type SourceCreator func() happydns.Source
// Source aggregates way of create a Source and information about it.
type Source struct {
Creator SourceCreator
Infos SourceInfos
}
// sources stores all existing Source in happyDNS.
var sources map[string]Source = map[string]Source{}
// RegisterSource declares the existence of the given Source.
func RegisterSource(creator SourceCreator, infos SourceInfos) {
baseType := reflect.Indirect(reflect.ValueOf(creator())).Type()
name := baseType.String()
@ -59,10 +63,12 @@ func RegisterSource(creator SourceCreator, infos SourceInfos) {
}
}
// GetSources retrieves the list of all existing Sources.
func GetSources() *map[string]Source {
return &sources
}
// FindSource returns the Source corresponding to the given name, or an error if it doesn't exist.
func FindSource(name string) (happydns.Source, error) {
src, ok := sources[name]
if !ok {

View File

@ -38,6 +38,7 @@ import (
"git.happydns.org/happydns/utils"
)
// DiffZones extracts the differences between the a and b lists.
func DiffZones(a []dns.RR, b []dns.RR, skipDNSSEC bool) (toAdd []dns.RR, toDel []dns.RR) {
loopDel:
for _, rrA := range a {
@ -70,6 +71,7 @@ loopAdd:
return
}
// DiffZone computes the difference between the current online zone and the one given in argument.
func DiffZone(s happydns.Source, domain *happydns.Domain, rrs []dns.RR, skipDNSSEC bool) (toAdd []dns.RR, toDel []dns.RR, err error) {
// Get the actuals RR-set
var current []dns.RR
@ -82,6 +84,7 @@ func DiffZone(s happydns.Source, domain *happydns.Domain, rrs []dns.RR, skipDNSS
return
}
// ApplyZone sent the given records to the Source.
func ApplyZone(s happydns.Source, domain *happydns.Domain, rrs []dns.RR, skipDNSSEC bool) (*dns.SOA, error) {
toAdd, toDel, err := DiffZone(s, domain, rrs, skipDNSSEC)
if err != nil {

View File

@ -38,18 +38,38 @@ import (
"git.happydns.org/happydns/model"
)
// SourceField
type SourceField struct {
Id string `json:"id"`
Type string `json:"type"`
Label string `json:"label,omitempty"`
Placeholder string `json:"placeholder,omitempty"`
Default string `json:"default,omitempty"`
Choices []string `json:"choices,omitempty"`
Required bool `json:"required,omitempty"`
Secret bool `json:"secret,omitempty"`
Description string `json:"description,omitempty"`
// Id is the field identifier.
Id string `json:"id"`
// Type is the string representation of the field's type.
Type string `json:"type"`
// Label is the title given to the field, displayed as <label> tag on the interface.
Label string `json:"label,omitempty"`
// Placeholder is the placeholder attribute of the corresponding <input> tag.
Placeholder string `json:"placeholder,omitempty"`
// Default is the preselected value or the default value in case the field is not filled by the user.
Default string `json:"default,omitempty"`
// Choices holds the differents choices shown to the user in <select> tag.
Choices []string `json:"choices,omitempty"`
// Required indicates whether the field has to be filled or not.
Required bool `json:"required,omitempty"`
// Secret indicates if the field contains sensitive information such as API key, in order to hide
// the field when not needed. When typing, it doesn't hide characters like in password input.
Secret bool `json:"secret,omitempty"`
// Description stores an helpfull sentence describing the field.
Description string `json:"description,omitempty"`
}
// GenSourceField generates a generic SourceField based on the happydns tag.
func GenSourceField(field reflect.StructField) (f *SourceField) {
jsonTag := field.Tag.Get("json")
jsonTuples := strings.Split(jsonTag, ",")
@ -96,6 +116,7 @@ func GenSourceField(field reflect.StructField) (f *SourceField) {
return
}
// GenSourceFields generates corresponding SourceFields of the given Source.
func GenSourceFields(src happydns.Source) (fields []*SourceField) {
if src != nil {
srcMeta := reflect.Indirect(reflect.ValueOf(src)).Type()

View File

@ -38,40 +38,75 @@ import (
"git.happydns.org/happydns/model"
)
// SourceInfos describes the purpose of a user usable source.
type SourceInfos struct {
Name string `json:"name"`
// Name is the name displayed.
Name string `json:"name"`
// Description is a brief description of what the source is.
Description string `json:"description"`
}
// ListDomainsSource are functions to declare when we can retrives a list of managable domains from the given Source.
type ListDomainsSource interface {
// ListDomains retrieves the list of avaiable domains inside the Source.
ListDomains() ([]string, error)
}
// CustomForm is used to create a form with several steps when creating or updating source's settings.
type CustomForm struct {
BeforeText string `json:"beforeText,omitempty"`
SideText string `json:"sideText,omitempty"`
AfterText string `json:"afterText,omitempty"`
Fields []*SourceField `json:"fields"`
NextButtonText string `json:"nextButtonText,omitempty"`
NextEditButtonText string `json:"nextEditButtonText,omitempty"`
PreviousButtonText string `json:"previousButtonText,omitempty"`
PreviousEditButtonText string `json:"previousEditButtonText,omitempty"`
NextButtonLink string `json:"nextButtonLink,omitempty"`
NextButtonState int32 `json:"nextButtonState,omitempty"`
PreviousButtonState int32 `json:"previousButtonState,omitempty"`
// BeforeText is the text presented before the fields.
BeforeText string `json:"beforeText,omitempty"`
// SideText is displayed in the sidebar, after any already existing text. When a sidebar is avaiable.
SideText string `json:"sideText,omitempty"`
// AfterText is the text presented after the fields and before the buttons
AfterText string `json:"afterText,omitempty"`
// Fields are the fields presented to the User.
Fields []*SourceField `json:"fields"`
// NextButtonText is the next button content.
NextButtonText string `json:"nextButtonText,omitempty"`
// NextEditButtonText is the next button content when updating the settings (if not set, NextButtonText is used instead).
NextEditButtonText string `json:"nextEditButtonText,omitempty"`
// PreviousButtonText is previous/cancel button content.
PreviousButtonText string `json:"previousButtonText,omitempty"`
// PreviousEditButtonText is the previous/cancel button content when updating the settings (if not set, NextButtonText is used instead).
PreviousEditButtonText string `json:"previousEditButtonText,omitempty"`
// NextButtonLink is the target of the next button, exclusive with NextButtonState.
NextButtonLink string `json:"nextButtonLink,omitempty"`
// NextButtonState is the step number asked when submiting the form.
NextButtonState int32 `json:"nextButtonState,omitempty"`
// PreviousButtonState is the step number to go when hitting the previous button.
PreviousButtonState int32 `json:"previousButtonState,omitempty"`
}
// GenRecallID
type GenRecallID func() int64
// CustomSettingsForm are functions to declare when we want to display a custom user experience when asking for Source's settings.
type CustomSettingsForm interface {
// DisplaySettingsForm generates the CustomForm corresponding to the asked target state.
DisplaySettingsForm(int32, *config.Options, *happydns.Session, GenRecallID) (*CustomForm, error)
}
var (
DoneForm = errors.New("Done")
// DoneForm is the error raised when there is no more step to display, and edition is OK.
DoneForm = errors.New("Done")
// CancelForm is the error raised when there is no more step to display and should redirect to the previous page.
CancelForm = errors.New("Cancel")
)
// GetSourceCapabilities lists available capabilities for the given Source.
func GetSourceCapabilities(src happydns.Source) (caps []string) {
if _, ok := src.(ListDomainsSource); ok {
caps = append(caps, "ListDomains")
@ -84,6 +119,7 @@ func GetSourceCapabilities(src happydns.Source) (caps []string) {
return
}
// GenDefaultSettingsForm generates a generic CustomForm presenting all the fields in one page.
func GenDefaultSettingsForm(src happydns.Source) *CustomForm {
return &CustomForm{
Fields: GenSourceFields(src),

View File

@ -334,6 +334,7 @@ func init() {
})
}
// fwd_request proxifies the given Request to the fwd URL.
func fwd_request(w http.ResponseWriter, r *http.Request, fwd string) {
if u, err := url.Parse(fwd); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -36,48 +36,130 @@ import (
)
type Storage interface {
// DoMigration is the first function called.
DoMigration() error
// Tidy should optimize the database, looking for orphan records, ...
Tidy() error
// Close shutdown the connection with the database and releases all structure.
Close() error
// DOMAINS ----------------------------------------------------
// GetDomains retrieves all Domains associated to the given User.
GetDomains(u *happydns.User) (happydns.Domains, error)
// GetDomain retrieves the Domain with the given id and owned by the given User.
GetDomain(u *happydns.User, id int64) (*happydns.Domain, error)
// GetDomainByDN is like GetDomain but look for the domain name instead of identifier.
GetDomainByDN(u *happydns.User, dn string) (*happydns.Domain, error)
// DomainExists looks if the given domain name alread exists in the database.
DomainExists(dn string) bool
// CreateDomain creates a record in the database for the given Domain.
CreateDomain(u *happydns.User, z *happydns.Domain) error
// UpdateDomain updates the fields of the given Domain.
UpdateDomain(z *happydns.Domain) error
// UpdateDomainOwner updates the owner of the given Domain.
UpdateDomainOwner(z *happydns.Domain, newOwner *happydns.User) error
// DeleteDomain removes the given Domain from the database.
DeleteDomain(z *happydns.Domain) error
// ClearDomains deletes all Domains present in the database.
ClearDomains() error
// SESSIONS ---------------------------------------------------
// GetSession retrieves the Session with the given identifier.
GetSession(id []byte) (*happydns.Session, error)
// CreateSession creates a record in the database for the given Session.
CreateSession(session *happydns.Session) error
// UpdateSession updates the fields of the given Session.
UpdateSession(session *happydns.Session) error
// DeleteSession removes the given Session from the database.
DeleteSession(session *happydns.Session) error
// ClearSessions deletes all Sessions present in the database.
ClearSessions() error
// SOURCES ----------------------------------------------------
// GetSourceMetas retrieves source's metadatas of all sources own by the given User.
GetSourceMetas(u *happydns.User) ([]happydns.SourceMeta, error)
GetSource(u *happydns.User, id int64) (*happydns.SourceCombined, error)
// GetSourceMeta retrieves the metadatas for the Source with the given identifier and owner.
GetSourceMeta(u *happydns.User, id int64) (*happydns.SourceMeta, error)
// GetSource retrieves the full Source with the given identifier and owner.
GetSource(u *happydns.User, id int64) (*happydns.SourceCombined, error)
// CreateSource creates a record in the database for the given Source.
CreateSource(u *happydns.User, s happydns.Source, comment string) (*happydns.SourceCombined, error)
// UpdateSource updates the fields of the given Source.
UpdateSource(s *happydns.SourceCombined) error
// UpdateSourceOwner updates the owner of the given Source.
UpdateSourceOwner(s *happydns.SourceCombined, newOwner *happydns.User) error
// DeleteSource removes the given Source from the database.
DeleteSource(s *happydns.SourceMeta) error
// ClearSources deletes all Sources present in the database.
ClearSources() error
// USERS ------------------------------------------------------
// GetUsers retrieves the list of known Users.
GetUsers() (happydns.Users, error)
// GetUser retrieves the User with the given identifier.
GetUser(id int64) (*happydns.User, error)
// GetUserByEmail retrives the User with the given email address.
GetUserByEmail(email string) (*happydns.User, error)
// UserExists checks if the given email address is already associated to an User.
UserExists(email string) bool
// CreateUser creates a record in the database for the given User.
CreateUser(user *happydns.User) error
// UpdateUser updates the fields of the given User.
UpdateUser(user *happydns.User) error
// DeleteUser removes the given User from the database.
DeleteUser(user *happydns.User) error
// ClearUsers deletes all Users present in the database.
ClearUsers() error
GetZone(id int64) (*happydns.Zone, error)
// ZONES ------------------------------------------------------
// GetZoneMeta retrives metadatas of the Zone with the given identifier.
GetZoneMeta(id int64) (*happydns.ZoneMeta, error)
// GetZone retrieves the full Zone (including Services and metadatas) which have the given identifier.
GetZone(id int64) (*happydns.Zone, error)
// CreateZone creates a record in the database for the given Zone.
CreateZone(zone *happydns.Zone) error
// UpdateZone updates the fields of the given Zone.
UpdateZone(zone *happydns.Zone) error
// DeleteZone removes the given Zone from the database.
DeleteZone(zone *happydns.Zone) error
// ClearZones deletes all Zones present in the database.
ClearZones() error
}