Refactor the whole Go project for a better directory architecture

This commit is contained in:
nemunaire 2020-04-05 13:07:42 +02:00
parent 30aa02e6e2
commit 7b9544b3db
26 changed files with 667 additions and 479 deletions

View File

@ -12,7 +12,8 @@ import (
"github.com/julienschmidt/httprouter"
"git.happydns.org/happydns/struct"
"git.happydns.org/happydns/model"
"git.happydns.org/happydns/storage"
)
type Response interface {
@ -58,8 +59,7 @@ func (r APIErrorResponse) WriteResponse(w http.ResponseWriter) {
http.Error(w, fmt.Sprintf("{\"errmsg\":%q}", r.err.Error()), r.status)
}
func apiHandler(f func(httprouter.Params, io.Reader) (Response)) func(http.ResponseWriter, *http.Request, httprouter.Params) {
func apiHandler(f func(httprouter.Params, io.Reader) Response) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if addr := r.Header.Get("X-Forwarded-For"); addr != "" {
r.RemoteAddr = addr
@ -76,7 +76,7 @@ func apiHandler(f func(httprouter.Params, io.Reader) (Response)) func(http.Respo
}
}
func apiAuthHandler(f func(happydns.User, httprouter.Params, io.Reader) (Response)) func(http.ResponseWriter, *http.Request, httprouter.Params) {
func apiAuthHandler(f func(*happydns.User, httprouter.Params, io.Reader) Response) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if addr := r.Header.Get("X-Forwarded-For"); addr != "" {
r.RemoteAddr = addr
@ -91,22 +91,22 @@ func apiAuthHandler(f func(happydns.User, httprouter.Params, io.Reader) (Respons
if flds := strings.Fields(r.Header.Get("Authorization")); len(flds) != 2 || flds[0] != "Bearer" {
APIErrorResponse{
err: errors.New("Authorization required"),
err: errors.New("Authorization required"),
status: http.StatusUnauthorized,
}.WriteResponse(w)
} else if sessionid, err := base64.StdEncoding.DecodeString(flds[1]); err != nil {
APIErrorResponse{
err: err,
err: err,
status: http.StatusUnauthorized,
}.WriteResponse(w)
} else if session, err := happydns.GetSession(sessionid); err != nil {
} else if session, err := storage.MainStore.GetSession(sessionid); err != nil {
APIErrorResponse{
err: err,
err: err,
status: http.StatusUnauthorized,
}.WriteResponse(w)
} else if std, err := happydns.GetUser(int(session.IdUser)); err != nil {
} else if std, err := storage.MainStore.GetUser(int(session.IdUser)); err != nil {
APIErrorResponse{
err: err,
err: err,
status: http.StatusUnauthorized,
}.WriteResponse(w)
} else {

View File

@ -5,7 +5,7 @@ import (
"github.com/julienschmidt/httprouter"
"git.happydns.org/happydns/struct"
"git.happydns.org/happydns/model"
)
func init() {

View File

@ -8,19 +8,20 @@ import (
"github.com/julienschmidt/httprouter"
"git.happydns.org/happydns/struct"
"git.happydns.org/happydns/model"
"git.happydns.org/happydns/storage"
)
var AuthFunc = checkAuth
func init() {
router.GET("/api/users/auth", apiAuthHandler(validateAuthToken))
router.POST("/api/users/auth", apiHandler(func(ps httprouter.Params, b io.Reader) (Response) {
router.POST("/api/users/auth", apiHandler(func(ps httprouter.Params, b io.Reader) Response {
return AuthFunc(ps, b)
}))
}
func validateAuthToken(u happydns.User, _ httprouter.Params, _ io.Reader) (Response) {
func validateAuthToken(u *happydns.User, _ httprouter.Params, _ io.Reader) Response {
return APIResponse{
response: u,
}
@ -39,18 +40,24 @@ func dummyAuth(_ httprouter.Params, body io.Reader) Response {
}
}
if user, err := happydns.GetUserByEmail(lf.Email); err != nil {
if user, err := storage.MainStore.GetUserByEmail(lf.Email); err != nil {
return APIErrorResponse{
err: err,
}
} else {
session, err := user.NewSession()
session, err := happydns.NewSession(user)
if err != nil {
return APIErrorResponse{
err: err,
}
}
if err := storage.MainStore.CreateSession(session); err != nil {
return APIErrorResponse{
err: err,
}
}
res := map[string]interface{}{}
res["status"] = "OK"
res["id_session"] = session.Id
@ -69,23 +76,29 @@ func checkAuth(_ httprouter.Params, body io.Reader) Response {
}
}
if user, err := happydns.GetUserByEmail(lf.Email); err != nil {
if user, err := storage.MainStore.GetUserByEmail(lf.Email); err != nil {
return APIErrorResponse{
err: err,
}
} else if !user.CheckAuth(lf.Password) {
return APIErrorResponse{
err: errors.New(`{"status": "Invalid username or password"}`),
err: errors.New(`{"status": "Invalid username or password"}`),
status: http.StatusUnauthorized,
}
} else {
session, err := user.NewSession()
session, err := happydns.NewSession(user)
if err != nil {
return APIErrorResponse{
err: err,
}
}
if err := storage.MainStore.CreateSession(session); err != nil {
return APIErrorResponse{
err: err,
}
}
res := map[string]interface{}{}
res["status"] = "OK"
res["id_session"] = session.Id

View File

@ -7,7 +7,8 @@ import (
"github.com/julienschmidt/httprouter"
"git.happydns.org/happydns/struct"
"git.happydns.org/happydns/model"
"git.happydns.org/happydns/storage"
)
func init() {
@ -16,7 +17,7 @@ func init() {
}
func listUsers(_ httprouter.Params, _ io.Reader) Response {
if users, err := happydns.GetUsers(); err != nil {
if users, err := storage.MainStore.GetUsers(); err != nil {
return APIErrorResponse{
err: err,
}
@ -28,8 +29,8 @@ func listUsers(_ httprouter.Params, _ io.Reader) Response {
}
type UploadedUser struct {
Email string
Password string
Email string
Password string
}
func registerUser(p httprouter.Params, body io.Reader) Response {
@ -51,6 +52,10 @@ func registerUser(p httprouter.Params, body io.Reader) Response {
return APIErrorResponse{
err: err,
}
} else if err := storage.MainStore.CreateUser(user); err != nil {
return APIErrorResponse{
err: err,
}
} else {
return APIResponse{
response: user,

View File

@ -13,7 +13,8 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/miekg/dns"
"git.happydns.org/happydns/struct"
"git.happydns.org/happydns/model"
"git.happydns.org/happydns/storage"
)
func init() {
@ -26,8 +27,8 @@ func init() {
router.DELETE("/api/zones/:zone/rr", apiAuthHandler(zoneHandler(delRR)))
}
func getZones(u happydns.User, p httprouter.Params, body io.Reader) Response {
if zones, err := u.GetZones(); err != nil {
func getZones(u *happydns.User, p httprouter.Params, body io.Reader) Response {
if zones, err := storage.MainStore.GetZones(u); err != nil {
return APIErrorResponse{
err: err,
}
@ -38,7 +39,7 @@ func getZones(u happydns.User, p httprouter.Params, body io.Reader) Response {
}
}
func addZone(u happydns.User, p httprouter.Params, body io.Reader) Response {
func addZone(u *happydns.User, p httprouter.Params, body io.Reader) Response {
var uz happydns.Zone
err := json.NewDecoder(body).Decode(&uz)
if err != nil {
@ -61,23 +62,23 @@ func addZone(u happydns.User, p httprouter.Params, body io.Reader) Response {
uz.KeyName = uz.KeyName + "."
}
if happydns.ZoneExists(uz.DomainName) {
if storage.MainStore.ZoneExists(uz.DomainName) {
return APIErrorResponse{
err: errors.New("This zone already exists."),
}
} else if zone, err := uz.NewZone(u); err != nil {
} else if err := storage.MainStore.CreateZone(u, &uz); err != nil {
return APIErrorResponse{
err: err,
}
} else {
return APIResponse{
response: zone,
response: uz,
}
}
}
func delZone(zone happydns.Zone, body io.Reader) Response {
if _, err := zone.Delete(); err != nil {
func delZone(zone *happydns.Zone, body io.Reader) Response {
if err := storage.MainStore.DeleteZone(zone); err != nil {
return APIErrorResponse{
err: err,
}
@ -88,9 +89,9 @@ func delZone(zone happydns.Zone, body io.Reader) Response {
}
}
func zoneHandler(f func(happydns.Zone, io.Reader) Response) func(happydns.User, httprouter.Params, io.Reader) Response {
return func(u happydns.User, ps httprouter.Params, body io.Reader) Response {
if zone, err := u.GetZoneByDN(ps.ByName("zone")); err != nil {
func zoneHandler(f func(*happydns.Zone, io.Reader) Response) func(*happydns.User, httprouter.Params, io.Reader) Response {
return func(u *happydns.User, ps httprouter.Params, body io.Reader) Response {
if zone, err := storage.MainStore.GetZoneByDN(u, ps.ByName("zone")); err != nil {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("Domain not found"),
@ -101,7 +102,7 @@ func zoneHandler(f func(happydns.Zone, io.Reader) Response) func(happydns.User,
}
}
func getZone(zone happydns.Zone, body io.Reader) Response {
func getZone(zone *happydns.Zone, body io.Reader) Response {
return APIResponse{
response: zone,
}
@ -117,13 +118,13 @@ func normalizeNSServer(srv string) string {
}
}
func axfrZone(zone happydns.Zone, body io.Reader) Response {
func axfrZone(zone *happydns.Zone, body io.Reader) Response {
d := net.Dialer{}
con, err := d.Dial("tcp", normalizeNSServer(zone.Server))
if err != nil {
return APIErrorResponse{
status: http.StatusInternalServerError,
err: err,
err: err,
}
}
defer con.Close()
@ -133,7 +134,7 @@ func axfrZone(zone happydns.Zone, body io.Reader) Response {
m.SetAxfr(zone.DomainName)
m.SetTsig(zone.KeyName, zone.KeyAlgo, 300, time.Now().Unix())
dnscon := &dns.Conn{Conn:con}
dnscon := &dns.Conn{Conn: con}
transfer := &dns.Transfer{Conn: dnscon, TsigSecret: map[string]string{zone.KeyName: zone.Base64KeyBlob()}}
c, err := transfer.In(m, normalizeNSServer(zone.Server))
@ -172,7 +173,7 @@ type uploadedRR struct {
RR string `json:"string"`
}
func addRR(zone happydns.Zone, body io.Reader) Response {
func addRR(zone *happydns.Zone, body io.Reader) Response {
var urr uploadedRR
err := json.NewDecoder(body).Decode(&urr)
if err != nil {
@ -217,7 +218,7 @@ func addRR(zone happydns.Zone, body io.Reader) Response {
}
}
func delRR(zone happydns.Zone, body io.Reader) Response {
func delRR(zone *happydns.Zone, body io.Reader) Response {
var urr uploadedRR
err := json.NewDecoder(body).Decode(&urr)
if err != nil {

3
generate.go Normal file
View File

@ -0,0 +1,3 @@
package main
//go:generate go run generators/gen_database_migration.go

View File

@ -0,0 +1,68 @@
package main
// +build ignore
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"text/template"
)
const tpl = `// Code generated by go generate. DO NOT EDIT.
package database // import "happydns.org/database"
const schemaVersion = {{ .MaxVersion }}
var schemaRevisions = map[uint16]string{
{{ range $constant, $content := .Map }}` + "\t" + `{{ $constant }}: ` + "`{{ $content }}`" + `,
{{ end }}}
`
var bundleTpl = template.Must(template.New("").Parse(tpl))
type valTpl struct {
MaxVersion uint64
Map map[uint16]string
}
func main() {
srcFiles, err := filepath.Glob("storage/mysql/schemas/*.sql")
if err != nil {
panic(err)
}
d := valTpl{
MaxVersion: 0,
Map: map[uint16]string{},
}
for _, srcFile := range srcFiles {
data, err := ioutil.ReadFile(srcFile)
if err != nil {
panic(err)
}
v, err := strconv.ParseUint(strings.TrimSuffix(path.Base(srcFile), ".sql"), 10, 16)
if err != nil {
panic(err)
}
d.Map[uint16(v)] = string(data)
if v > d.MaxVersion {
d.MaxVersion = v
}
}
f, err := os.Create("storage/mysql/schemas.go")
if err != nil {
panic(err)
}
defer f.Close()
bundleTpl.Execute(f, d)
}

1
go.sum
View File

@ -18,5 +18,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

17
main.go
View File

@ -9,7 +9,8 @@ import (
"strings"
"git.happydns.org/happydns/api"
"git.happydns.org/happydns/struct"
"git.happydns.org/happydns/storage"
"git.happydns.org/happydns/storage/mysql"
)
type ResponseWriterPrefix struct {
@ -56,7 +57,7 @@ func main() {
// Read parameters from command line
flag.StringVar(&DevProxy, "dev", DevProxy, "Proxify traffic to this host for static assets")
var bind = flag.String("bind", ":8081", "Bind port/socket")
var dsn = flag.String("dsn", happydns.DSNGenerator(), "DSN to connect to the MySQL server")
var dsn = flag.String("dsn", database.DSNGenerator(), "DSN to connect to the MySQL server")
var baseURL = flag.String("baseurl", "/", "URL prepended to each URL")
flag.StringVar(&api.DefaultNameServer, "defaultns", api.DefaultNameServer, "Adress to the default name server")
flag.Parse()
@ -72,14 +73,16 @@ func main() {
// Initialize contents
log.Println("Opening database...")
if err := happydns.DBInit(*dsn); err != nil {
if store, err := database.NewMySQLStorage(*dsn); err != nil {
log.Fatal("Cannot open the database: ", err)
} else {
storage.MainStore = store
}
defer happydns.DBClose()
defer storage.MainStore.Close()
log.Println("Creating database...")
if err := happydns.DBCreate(); err != nil {
log.Fatal("Cannot create database: ", err)
log.Println("Do database migrations...")
if err := storage.MainStore.DoMigration(); err != nil {
log.Fatal("Cannot migrate database: ", err)
}
// Serve content

26
model/session.go Normal file
View File

@ -0,0 +1,26 @@
package happydns
import (
"crypto/rand"
"time"
)
type Session struct {
Id []byte `json:"id"`
IdUser int64 `json:"login"`
Time time.Time `json:"time"`
}
func NewSession(user *User) (s *Session, err error) {
session_id := make([]byte, 255)
_, err = rand.Read(session_id)
if err == nil {
s = &Session{
Id: session_id,
IdUser: user.Id,
Time: time.Now(),
}
}
return
}

52
model/user.go Normal file
View File

@ -0,0 +1,52 @@
package happydns
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha512"
"time"
)
type User struct {
Id int64 `json:"id"`
Email string `json:"email"`
Password []byte
Salt []byte
RegistrationTime *time.Time `json:"registration_time"`
}
type Users []*User
func GenPassword(password string, salt []byte) []byte {
return hmac.New(sha512.New512_224, []byte(password)).Sum([]byte(salt))
}
func NewUser(email string, password string) (*User, error) {
salt := make([]byte, 64)
if _, err := rand.Read(salt); err != nil {
return nil, err
}
hashedpass := GenPassword(password, salt)
t := time.Now()
return &User{
Id: 0,
Email: email,
Password: hashedpass,
Salt: salt,
RegistrationTime: &t,
}, nil
}
func (u *User) CheckAuth(password string) bool {
pass := GenPassword(password, u.Salt)
if len(pass) != len(u.Password) {
return false
} else {
for k := range pass {
if pass[k] != u.Password[k] {
return false
}
}
return true
}
}

34
model/zone.go Normal file
View File

@ -0,0 +1,34 @@
package happydns
import (
"encoding/base64"
)
type Zone struct {
Id int64 `json:"id"`
IdUser int64
DomainName string `json:"domain"`
Server string `json:"server,omitempty"`
KeyName string `json:"keyname,omitempty"`
KeyAlgo string `json:"algorithm,omitempty"`
KeyBlob []byte `json:"keyblob,omitempty"`
StorageFacility string `json:"storage_facility,omitempty"`
}
type Zones []*Zone
func NewZone(u User, dn, server, keyname, algo string, keyblob []byte, storage string) *Zone {
return &Zone{
IdUser: u.Id,
DomainName: dn,
Server: server,
KeyName: keyname,
KeyAlgo: algo,
KeyBlob: keyblob,
StorageFacility: storage,
}
}
func (z *Zone) Base64KeyBlob() string {
return base64.StdEncoding.EncodeToString(z.KeyBlob)
}

35
storage/interface.go Normal file
View File

@ -0,0 +1,35 @@
package storage // import "happydns.org/storage"
import (
"git.happydns.org/happydns/model"
)
type Storage interface {
DoMigration() error
Close() error
GetSession(id []byte) (*happydns.Session, error)
CreateSession(session *happydns.Session) error
UpdateSession(session *happydns.Session) error
DeleteSession(session *happydns.Session) error
ClearSession() error
GetUsers() (happydns.Users, error)
GetUser(id int) (*happydns.User, error)
GetUserByEmail(email string) (*happydns.User, error)
UserExists(email string) bool
CreateUser(user *happydns.User) error
UpdateUser(user *happydns.User) error
DeleteUser(user *happydns.User) error
ClearUsers() error
GetZones(u *happydns.User) (happydns.Zones, error)
GetZone(u *happydns.User, id int) (*happydns.Zone, error)
GetZoneByDN(u *happydns.User, dn string) (*happydns.Zone, error)
ZoneExists(dn string) bool
CreateZone(u *happydns.User, z *happydns.Zone) error
UpdateZone(z *happydns.Zone) error
UpdateZoneOwner(z *happydns.Zone, newOwner *happydns.User) error
DeleteZone(z *happydns.Zone) error
ClearZones() error
}

84
storage/mysql/database.go Normal file
View File

@ -0,0 +1,84 @@
package database // import "happydns.org/database"
import (
"database/sql"
"log"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
)
type MySQLStorage struct {
db *sql.DB
}
// NewMySQLStorage establishes the connection to the database
func NewMySQLStorage(dsn string) (*MySQLStorage, error) {
if db, err := sql.Open("mysql", dsn+"?parseTime=true&foreign_key_checks=1"); err != nil {
return nil, err
} else {
_, err := db.Exec(`SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO';`)
for i := 0; err != nil && i < 45; i += 1 {
if _, err = db.Exec(`SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO';`); err != nil && i <= 45 {
log.Println("An error occurs when trying to connect to DB, will retry in 2 seconds: ", err)
time.Sleep(2 * time.Second)
}
}
if err != nil {
return nil, err
}
return &MySQLStorage{db}, nil
}
}
func (s *MySQLStorage) DoMigration() error {
var currentVersion uint16
s.db.QueryRow(`SELECT version FROM schema_version`).Scan(&currentVersion)
log.Println("Current schema version:", currentVersion)
log.Println("Latest schema version:", schemaVersion)
for version := currentVersion + 1; version <= schemaVersion; version++ {
log.Println("Migrating to version:", version)
tx, err := s.db.Begin()
if err != nil {
return err
}
rawSQL := schemaRevisions[version]
for _, request := range strings.Split(rawSQL, ";") {
if len(strings.TrimSpace(request)) == 0 {
continue
}
_, err = tx.Exec(request)
if err != nil {
tx.Rollback()
return err
}
}
if _, err := tx.Exec(`delete from schema_version`); err != nil {
tx.Rollback()
return err
}
if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES (?)`, version); err != nil {
tx.Rollback()
return err
}
if err := tx.Commit(); err != nil {
return err
}
}
return nil
}
func (s *MySQLStorage) Close() error {
return s.db.Close()
}

31
storage/mysql/db.go Normal file
View File

@ -0,0 +1,31 @@
package database
import (
"os"
)
// DSNGenerator returns DSN filed with values from environment
func DSNGenerator() string {
db_user := "happydns"
db_password := "happydns"
db_host := ""
db_db := "happydns"
if v, exists := os.LookupEnv("MYSQL_HOST"); exists {
db_host = v
}
if v, exists := os.LookupEnv("MYSQL_PASSWORD"); exists {
db_password = v
} else if v, exists := os.LookupEnv("MYSQL_ROOT_PASSWORD"); exists {
db_user = "root"
db_password = v
}
if v, exists := os.LookupEnv("MYSQL_USER"); exists {
db_user = v
}
if v, exists := os.LookupEnv("MYSQL_DATABASE"); exists {
db_db = v
}
return db_user + ":" + db_password + "@" + db_host + "/" + db_db
}

39
storage/mysql/schemas.go Normal file
View File

@ -0,0 +1,39 @@
// Code generated by go generate. DO NOT EDIT.
package database // import "happydns.org/database"
const schemaVersion = 1
var schemaRevisions = map[uint16]string{
1: `CREATE TABLE schema_version (
version SMALLINT UNSIGNED NOT NULL
);
CREATE TABLE users (
id_user INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
password BINARY(92) NOT NULL,
salt BINARY(64) NOT NULL,
registration_time TIMESTAMP NOT NULL
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
CREATE TABLE user_sessions (
id_session BLOB(255) NOT NULL,
id_user INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(id_user) REFERENCES users(id_user)
);
CREATE TABLE zones (
id_zone INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
id_user INTEGER NOT NULL,
domain VARCHAR(255) NOT NULL,
server VARCHAR(255),
key_name VARCHAR(255) NOT NULL,
key_algo ENUM("hmac-md5.sig-alg.reg.int.", "hmac-sha1.", "hmac-sha224.", "hmac-sha256.", "hmac-sha384.", "hmac-sha512.") NOT NULL DEFAULT "hmac-sha256.",
key_blob BLOB NOT NULL,
storage_facility ENUM("live", "history") NOT NULL DEFAULT "live",
FOREIGN KEY(id_user) REFERENCES users(id_user)
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
`,
}

View File

@ -0,0 +1,30 @@
CREATE TABLE schema_version (
version SMALLINT UNSIGNED NOT NULL
);
CREATE TABLE users (
id_user INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
password BINARY(92) NOT NULL,
salt BINARY(64) NOT NULL,
registration_time TIMESTAMP NOT NULL
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
CREATE TABLE user_sessions (
id_session BLOB(255) NOT NULL,
id_user INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(id_user) REFERENCES users(id_user)
);
CREATE TABLE zones (
id_zone INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
id_user INTEGER NOT NULL,
domain VARCHAR(255) NOT NULL,
server VARCHAR(255),
key_name VARCHAR(255) NOT NULL,
key_algo ENUM("hmac-md5.sig-alg.reg.int.", "hmac-sha1.", "hmac-sha224.", "hmac-sha256.", "hmac-sha384.", "hmac-sha512.") NOT NULL DEFAULT "hmac-sha256.",
key_blob BLOB NOT NULL,
storage_facility ENUM("live", "history") NOT NULL DEFAULT "live",
FOREIGN KEY(id_user) REFERENCES users(id_user)
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;

40
storage/mysql/session.go Normal file
View File

@ -0,0 +1,40 @@
package database
import (
"crypto/rand"
"git.happydns.org/happydns/model"
)
func (s *MySQLStorage) GetSession(id []byte) (session *happydns.Session, err error) {
session = &happydns.Session{}
err = s.db.QueryRow("SELECT id_session, id_user, time FROM user_sessions WHERE id_session=?", id).Scan(&session.Id, &session.IdUser, &session.Time)
return
}
func (s *MySQLStorage) CreateSession(session *happydns.Session) error {
session_id := make([]byte, 255)
if _, err := rand.Read(session_id); err != nil {
return err
} else if _, err := s.db.Exec("INSERT INTO user_sessions (id_session, id_user, time) VALUES (?, ?, ?)", session_id, session.IdUser, session.Time); err != nil {
return err
} else {
session.Id = session_id
return nil
}
}
func (s *MySQLStorage) UpdateSession(session *happydns.Session) error {
_, err := s.db.Exec(`UPDATE user_sessions SET id_user = ?, time = ? WHERE id_session = ?`, session.IdUser, session.Time, session.Id)
return err
}
func (s *MySQLStorage) DeleteSession(session *happydns.Session) error {
_, err := s.db.Exec("DELETE FROM user_sessions WHERE id_session = ?", session.Id)
return err
}
func (s *MySQLStorage) ClearSession() error {
_, err := s.db.Exec("DELETE FROM user_sessions")
return err
}

70
storage/mysql/user.go Normal file
View File

@ -0,0 +1,70 @@
package database
import (
"git.happydns.org/happydns/model"
)
func (s *MySQLStorage) GetUsers() (users happydns.Users, err error) {
if rows, errr := s.db.Query("SELECT id_user, email, password, salt, registration_time FROM users"); errr != nil {
return nil, errr
} else {
defer rows.Close()
for rows.Next() {
var u happydns.User
if err = rows.Scan(&u.Id, &u.Email, &u.Password, &u.Salt, &u.RegistrationTime); err != nil {
return
}
users = append(users, &u)
}
if err = rows.Err(); err != nil {
return
}
return
}
}
func (s *MySQLStorage) GetUser(id int) (u *happydns.User, err error) {
u = &happydns.User{}
err = s.db.QueryRow("SELECT id_user, email, password, salt, registration_time FROM users WHERE id_user=?", id).Scan(&u.Id, &u.Email, &u.Password, &u.Salt, &u.RegistrationTime)
return
}
func (s *MySQLStorage) GetUserByEmail(email string) (u *happydns.User, err error) {
u = &happydns.User{}
err = s.db.QueryRow("SELECT id_user, email, password, salt, registration_time FROM users WHERE email=?", email).Scan(&u.Id, &u.Email, &u.Password, &u.Salt, &u.RegistrationTime)
return
}
func (s *MySQLStorage) UserExists(email string) bool {
var z int
err := s.db.QueryRow("SELECT 1 FROM users WHERE email=?", email).Scan(&z)
return err == nil && z == 1
}
func (s *MySQLStorage) CreateUser(u *happydns.User) error {
if res, err := s.db.Exec("INSERT INTO users (email, password, salt, registration_time) VALUES (?, ?, ?, ?)", u.Email, u.Password, u.Salt, u.RegistrationTime); err != nil {
return err
} else if sid, err := res.LastInsertId(); err != nil {
return err
} else {
u.Id = sid
return err
}
}
func (s *MySQLStorage) UpdateUser(u *happydns.User) error {
_, err := s.db.Exec("UPDATE users SET email = ?, password = ?, salt = ?, registration_time = ? WHERE id_user = ?", u.Email, u.Password, u.Salt, u.RegistrationTime, u.Id)
return err
}
func (s *MySQLStorage) DeleteUser(u *happydns.User) error {
_, err := s.db.Exec("DELETE FROM users WHERE id_user = ?", u.Id)
return err
}
func (s *MySQLStorage) ClearUsers() error {
_, err := s.db.Exec("DELETE FROM users")
return err
}

81
storage/mysql/zone.go Normal file
View File

@ -0,0 +1,81 @@
package database
import (
"git.happydns.org/happydns/model"
)
func (s *MySQLStorage) GetZones(u *happydns.User) (zones happydns.Zones, err error) {
if rows, errr := s.db.Query("SELECT id_zone, id_user, domain, server, key_name, key_algo, key_blob, storage_facility FROM zones WHERE id_user = ?", u.Id); errr != nil {
return nil, errr
} else {
defer rows.Close()
for rows.Next() {
var z happydns.Zone
if err = rows.Scan(&z.Id, &z.IdUser, &z.DomainName, &z.Server, &z.KeyName, &z.KeyAlgo, &z.KeyBlob, &z.StorageFacility); err != nil {
return
}
zones = append(zones, &z)
}
if err = rows.Err(); err != nil {
return
}
return
}
}
func (s *MySQLStorage) GetZone(u *happydns.User, id int) (z *happydns.Zone, err error) {
z = &happydns.Zone{}
err = s.db.QueryRow("SELECT id_zone, id_user, domain, server, key_name, key_algo, key_blob, storage_facility FROM zones WHERE id_zone=? AND id_user=?", id, u.Id).Scan(&z.Id, &z.IdUser, &z.DomainName, &z.Server, &z.KeyName, &z.KeyAlgo, &z.KeyBlob, &z.StorageFacility)
return
}
func (s *MySQLStorage) GetZoneByDN(u *happydns.User, dn string) (z *happydns.Zone, err error) {
z = &happydns.Zone{}
err = s.db.QueryRow("SELECT id_zone, id_user, domain, server, key_name, key_algo, key_blob, storage_facility FROM zones WHERE domain=? AND id_user=?", dn, u.Id).Scan(&z.Id, &z.IdUser, &z.DomainName, &z.Server, &z.KeyName, &z.KeyAlgo, &z.KeyBlob, &z.StorageFacility)
return
}
func (s *MySQLStorage) ZoneExists(dn string) bool {
var z int
err := s.db.QueryRow("SELECT 1 FROM zones WHERE domain=?", dn).Scan(&z)
return err == nil && z == 1
}
func (s *MySQLStorage) CreateZone(u *happydns.User, z *happydns.Zone) error {
if res, err := s.db.Exec("INSERT INTO zones (id_user, domain, server, key_name, key_blob, storage_facility) VALUES (?, ?, ?, ?, ?, ?)", u.Id, z.DomainName, z.Server, z.KeyName, z.KeyBlob, z.StorageFacility); err != nil {
return err
} else if z.Id, err = res.LastInsertId(); err != nil {
return err
} else {
return nil
}
}
func (s *MySQLStorage) UpdateZone(z *happydns.Zone) error {
if _, err := s.db.Exec("UPDATE zones SET domain = ?, key_name = ?, key_algo = ?, key_blob = ?, storage_facility = ? WHERE id_zone = ?", z.DomainName, z.KeyName, z.KeyAlgo, z.KeyBlob, z.StorageFacility, z.Id); err != nil {
return err
} else {
return nil
}
}
func (s *MySQLStorage) UpdateZoneOwner(z *happydns.Zone, newOwner *happydns.User) error {
if _, err := s.db.Exec("UPDATE zones SET id_user = ? WHERE id_zone = ?", newOwner.Id, z.Id); err != nil {
return err
} else {
z.IdUser = newOwner.Id
return nil
}
}
func (s *MySQLStorage) DeleteZone(z *happydns.Zone) error {
_, err := s.db.Exec("DELETE FROM zones WHERE id_zone = ?", z.Id)
return err
}
func (s *MySQLStorage) ClearZones() error {
_, err := s.db.Exec("DELETE FROM zones")
return err
}

3
storage/singleton.go Normal file
View File

@ -0,0 +1,3 @@
package storage
var MainStore Storage

View File

@ -1,117 +0,0 @@
package happydns
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"log"
"os"
"time"
)
// db stores the connection to the database
var db *sql.DB
// DSNGenerator returns DSN filed with values from environment
func DSNGenerator() string {
db_user := "happydns"
db_password := "happydns"
db_host := ""
db_db := "happydns"
if v, exists := os.LookupEnv("MYSQL_HOST"); exists {
db_host = v
}
if v, exists := os.LookupEnv("MYSQL_PASSWORD"); exists {
db_password = v
} else if v, exists := os.LookupEnv("MYSQL_ROOT_PASSWORD"); exists {
db_user = "root"
db_password = v
}
if v, exists := os.LookupEnv("MYSQL_USER"); exists {
db_user = v
}
if v, exists := os.LookupEnv("MYSQL_DATABASE"); exists {
db_db = v
}
return db_user + ":" + db_password + "@" + db_host + "/" + db_db
}
// DBInit establishes the connection to the database
func DBInit(dsn string) (err error) {
if db, err = sql.Open("mysql", dsn+"?parseTime=true&foreign_key_checks=1"); err != nil {
return
}
_, err = db.Exec(`SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO';`)
for i := 0; err != nil && i < 45; i += 1 {
if _, err = db.Exec(`SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO';`); err != nil && i <= 45 {
log.Println("An error occurs when trying to connect to DB, will retry in 2 seconds: ", err)
time.Sleep(2 * time.Second)
}
}
return
}
// DBCreate creates all necessary tables used by the package
func DBCreate() error {
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS users(
id_user INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
password BINARY(92) NOT NULL,
salt BINARY(64) NOT NULL,
registration_time TIMESTAMP NOT NULL
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
`); err != nil {
return err
}
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS user_sessions(
id_session BLOB(255) NOT NULL,
id_user INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(id_user) REFERENCES users(id_user)
);
`); err != nil {
return err
}
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS zones(
id_zone INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
id_user INTEGER NOT NULL,
domain VARCHAR(255) NOT NULL,
server VARCHAR(255),
key_name VARCHAR(255) NOT NULL,
key_algo ENUM("hmac-md5.sig-alg.reg.int.", "hmac-sha1.", "hmac-sha224.", "hmac-sha256.", "hmac-sha384.", "hmac-sha512.") NOT NULL DEFAULT "hmac-sha256.",
key_blob BLOB NOT NULL,
storage_facility ENUM("live", "history") NOT NULL DEFAULT "live",
FOREIGN KEY(id_user) REFERENCES users(id_user)
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
`); err != nil {
return err
}
return nil
}
// DBClose closes the connection to the database
func DBClose() error {
return db.Close()
}
func DBPrepare(query string) (*sql.Stmt, error) {
return db.Prepare(query)
}
func DBQuery(query string, args ...interface{}) (*sql.Rows, error) {
return db.Query(query, args...)
}
func DBExec(query string, args ...interface{}) (sql.Result, error) {
return db.Exec(query, args...)
}
func DBQueryRow(query string, args ...interface{}) *sql.Row {
return db.QueryRow(query, args...)
}

View File

@ -1,58 +0,0 @@
package happydns
import (
"crypto/rand"
"time"
)
type Session struct {
Id []byte `json:"id"`
IdUser int64 `json:"login"`
Time time.Time `json:"time"`
}
func GetSession(id []byte) (s Session, err error) {
err = DBQueryRow("SELECT id_session, id_user, time FROM user_sessions WHERE id_session=?", id).Scan(&s.Id, &s.IdUser, &s.Time)
return
}
func (user User) NewSession() (Session, error) {
session_id := make([]byte, 255)
if _, err := rand.Read(session_id); err != nil {
return Session{}, err
} else if _, err := DBExec("INSERT INTO user_sessions (id_session, id_user, time) VALUES (?, ?, ?)", session_id, user.Id, time.Now()); err != nil {
return Session{}, err
} else {
return Session{session_id, user.Id, time.Now()}, nil
}
}
func (s Session) Update() (int64, error) {
if res, err := DBExec("UPDATE user_sessions SET id_user = ?, time = ? WHERE id_session = ?", s.IdUser, s.Time, s.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
func (s Session) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM user_sessions WHERE id_session = ?", s.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
func ClearSession() (int64, error) {
if res, err := DBExec("DELETE FROM user_sessions"); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}

View File

@ -1,117 +0,0 @@
package happydns
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha512"
"time"
)
type User struct {
Id int64 `json:"id"`
Email string `json:"email"`
password []byte
salt []byte
RegistrationTime *time.Time `json:"registration_time"`
}
func GetUsers() (users []User, err error) {
if rows, errr := DBQuery("SELECT id_user, email, password, salt, registration_time FROM users"); errr != nil {
return nil, errr
} else {
defer rows.Close()
for rows.Next() {
var u User
if err = rows.Scan(&u.Id, &u.Email, &u.password, &u.salt, &u.RegistrationTime); err != nil {
return
}
users = append(users, u)
}
if err = rows.Err(); err != nil {
return
}
return
}
}
func GetUser(id int) (u User, err error) {
err = DBQueryRow("SELECT id_user, email, password, salt, registration_time FROM users WHERE id_user=?", id).Scan(&u.Id, &u.Email, &u.password, &u.salt, &u.RegistrationTime)
return
}
func GetUserByEmail(email string) (u User, err error) {
err = DBQueryRow("SELECT id_user, email, password, salt, registration_time FROM users WHERE email=?", email).Scan(&u.Id, &u.Email, &u.password, &u.salt, &u.RegistrationTime)
return
}
func UserExists(email string) bool {
var z int
err := DBQueryRow("SELECT 1 FROM users WHERE email=?", email).Scan(&z)
return err == nil && z == 1
}
func GenPassword(password string, salt []byte) []byte {
return hmac.New(sha512.New512_224, []byte(password)).Sum([]byte(salt))
}
func NewUser(email string, password string) (User, error) {
salt := make([]byte, 64)
if _, err := rand.Read(salt); err != nil {
return User{}, err
}
t := time.Now()
hashedpass := GenPassword(password, salt)
if res, err := DBExec("INSERT INTO users (email, password, salt, registration_time) VALUES (?, ?, ?, ?)", email, hashedpass, salt, t); err != nil {
return User{}, err
} else if sid, err := res.LastInsertId(); err != nil {
return User{}, err
} else {
return User{sid, email, hashedpass, salt, &t}, nil
}
}
func (u *User) CheckAuth(password string) bool {
pass := GenPassword(password, u.salt)
if len(pass) != len(u.password) {
return false
} else {
for k := range pass {
if pass[k] != u.password[k] {
return false
}
}
return true
}
}
func (u *User) Update() (int64, error) {
if res, err := DBExec("UPDATE users SET email = ?, password = ?, salt = ?, registration_time = ? WHERE id_user = ?", u.Email, u.password, u.salt, u.RegistrationTime, u.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
func (u *User) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM users WHERE id_user = ?", u.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
func ClearUsers() (int64, error) {
if res, err := DBExec("DELETE FROM users"); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}

View File

@ -1,139 +0,0 @@
package happydns
import (
"encoding/base64"
)
type Zone struct {
Id int64 `json:"id"`
idUser int64
DomainName string `json:"domain"`
Server string `json:"server,omitempty"`
KeyName string `json:"keyname,omitempty"`
KeyAlgo string `json:"algorithm,omitempty"`
KeyBlob []byte `json:"keyblob,omitempty"`
StorageFacility string `json:"storage_facility,omitempty"`
}
func GetZones() (zones []Zone, err error) {
if rows, errr := DBQuery("SELECT id_zone, id_user, domain, server, key_name, key_algo, key_blob, storage_facility FROM zones"); errr != nil {
return nil, errr
} else {
defer rows.Close()
for rows.Next() {
var z Zone
if err = rows.Scan(&z.Id, &z.idUser, &z.DomainName, &z.Server, &z.KeyName, &z.KeyAlgo, &z.KeyBlob, &z.StorageFacility); err != nil {
return
}
zones = append(zones, z)
}
if err = rows.Err(); err != nil {
return
}
return
}
}
func (u *User) GetZones() (zones []Zone, err error) {
if rows, errr := DBQuery("SELECT id_zone, id_user, domain, server, key_name, key_algo, key_blob, storage_facility FROM zones WHERE id_user = ?", u.Id); errr != nil {
return nil, errr
} else {
defer rows.Close()
for rows.Next() {
var z Zone
if err = rows.Scan(&z.Id, &z.idUser, &z.DomainName, &z.Server, &z.KeyName, &z.KeyAlgo, &z.KeyBlob, &z.StorageFacility); err != nil {
return
}
zones = append(zones, z)
}
if err = rows.Err(); err != nil {
return
}
return
}
}
func GetZone(id int) (z Zone, err error) {
err = DBQueryRow("SELECT id_zone, id_user, domain, server, key_name, key_algo, key_blob, storage_facility FROM zones WHERE id_zone=?", id).Scan(&z.Id, &z.idUser, &z.DomainName, &z.Server, &z.KeyName, &z.KeyAlgo, &z.KeyBlob, &z.StorageFacility)
return
}
func (u *User) GetZone(id int) (z Zone, err error) {
err = DBQueryRow("SELECT id_zone, id_user, domain, server, key_name, key_algo, key_blob, storage_facility FROM zones WHERE id_zone=? AND id_user=?", id, u.Id).Scan(&z.Id, &z.idUser, &z.DomainName, &z.Server, &z.KeyName, &z.KeyAlgo, &z.KeyBlob, &z.StorageFacility)
return
}
func GetZoneByDN(dn string) (z Zone, err error) {
err = DBQueryRow("SELECT id_zone, id_user, domain, server, key_name, key_algo, key_blob, storage_facility FROM zones WHERE domain=?", dn).Scan(&z.Id, &z.idUser, &z.DomainName, &z.Server, &z.KeyName, &z.KeyAlgo, &z.KeyBlob, &z.StorageFacility)
return
}
func (u *User) GetZoneByDN(dn string) (z Zone, err error) {
err = DBQueryRow("SELECT id_zone, id_user, domain, server, key_name, key_algo, key_blob, storage_facility FROM zones WHERE domain=? AND id_user=?", dn, u.Id).Scan(&z.Id, &z.idUser, &z.DomainName, &z.Server, &z.KeyName, &z.KeyAlgo, &z.KeyBlob, &z.StorageFacility)
return
}
func ZoneExists(dn string) bool {
var z int
err := DBQueryRow("SELECT 1 FROM zones WHERE domain=?", dn).Scan(&z)
return err == nil && z == 1
}
func (z *Zone) NewZone(u User) (Zone, error) {
if res, err := DBExec("INSERT INTO zones (id_user, domain, server, key_name, key_blob, storage_facility) VALUES (?, ?, ?, ?, ?, ?)", u.Id, z.DomainName, z.Server, z.KeyName, z.KeyBlob, z.StorageFacility); err != nil {
return *z, err
} else if z.Id, err = res.LastInsertId(); err != nil {
return *z, err
} else {
return *z, nil
}
}
func (z *Zone) Update() (int64, error) {
if res, err := DBExec("UPDATE zones SET domain = ?, key_name = ?, key_algo = ?, key_blob = ?, storage_facility = ? WHERE id_zone = ?", z.DomainName, z.KeyName, z.KeyAlgo, z.KeyBlob, z.StorageFacility, z.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
func (z *Zone) UpdateOwner(u User) (int64, error) {
if res, err := DBExec("UPDATE zones SET id_user = ? WHERE id_zone = ?", u.Id, z.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
z.idUser = u.Id
return nb, err
}
}
func (z *Zone) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM zones WHERE id_zone = ?", z.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
func (z *Zone) Base64KeyBlob() string {
return base64.StdEncoding.EncodeToString(z.KeyBlob)
}
func ClearZones() (int64, error) {
if res, err := DBExec("DELETE FROM zones"); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}