Initial commit
This commit is contained in:
commit
6d60e29ddf
|
@ -0,0 +1 @@
|
|||
libredns
|
|
@ -0,0 +1,72 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
type Response interface {
|
||||
WriteResponse(http.ResponseWriter)
|
||||
}
|
||||
|
||||
type APIResponse struct {
|
||||
response interface{}
|
||||
}
|
||||
|
||||
func (r APIResponse) WriteResponse(w http.ResponseWriter) {
|
||||
if str, found := r.response.(string); found {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.WriteString(w, str)
|
||||
} else if bts, found := r.response.([]byte); found {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", "attachment")
|
||||
w.Header().Set("Content-Transfer-Encoding", "binary")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(bts)
|
||||
} else if j, err := json.Marshal(r.response); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
http.Error(w, fmt.Sprintf("{\"errmsg\":%q}", err), http.StatusInternalServerError)
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(j)
|
||||
}
|
||||
}
|
||||
|
||||
type APIErrorResponse struct {
|
||||
status int
|
||||
err error
|
||||
}
|
||||
|
||||
func (r APIErrorResponse) WriteResponse(w http.ResponseWriter) {
|
||||
if r.status == 0 {
|
||||
r.status = http.StatusBadRequest
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
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) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
if addr := r.Header.Get("X-Forwarded-For"); addr != "" {
|
||||
r.RemoteAddr = addr
|
||||
}
|
||||
log.Printf("%s \"%s %s\" [%s]\n", r.RemoteAddr, r.Method, r.URL.Path, r.UserAgent())
|
||||
|
||||
// Read the body
|
||||
if r.ContentLength < 0 || r.ContentLength > 6553600 {
|
||||
http.Error(w, fmt.Sprintf("{errmsg:\"Request too large or request size unknown\"}"), http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
f(ps, r.Body).WriteResponse(w)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
var router = httprouter.New()
|
||||
|
||||
func Router() *httprouter.Router {
|
||||
return router
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func init() {
|
||||
router.GET("/api/zones/", apiHandler(getZones))
|
||||
router.GET("/api/zones/:zone/", apiHandler(zoneHandler(axfrZone)))
|
||||
router.PUT("/api/zones/:zone/", apiHandler(zoneHandler(addRR)))
|
||||
router.DELETE("/api/zones/:zone/", apiHandler(zoneHandler(delRR)))
|
||||
}
|
||||
|
||||
func getZones(p httprouter.Params, body io.Reader) (Response) {
|
||||
return APIResponse{
|
||||
response: map[string][]string{
|
||||
"zones": []string{
|
||||
"adlin2020.p0m.fr.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func zoneHandler(f func(string, io.Reader) (Response)) func(httprouter.Params, io.Reader) (Response) {
|
||||
return func(ps httprouter.Params, body io.Reader) (Response) {
|
||||
zone := ps.ByName("zone")
|
||||
|
||||
if zone[len(zone)-1] != '.' {
|
||||
return APIErrorResponse{
|
||||
err: errors.New("Not a valid full qualified domain name"),
|
||||
}
|
||||
}
|
||||
|
||||
return f(zone, body)
|
||||
}
|
||||
}
|
||||
|
||||
func axfrZone(zone string, body io.Reader) (Response) {
|
||||
t := new(dns.Transfer)
|
||||
|
||||
m := new(dns.Msg)
|
||||
t.TsigSecret = map[string]string{"ddns.": "so6ZGir4GPAqINNh9U5c3A=="}
|
||||
m.SetAxfr(zone)
|
||||
m.SetTsig("ddns.", dns.HmacSHA256, 300, time.Now().Unix())
|
||||
|
||||
c, err := t.In(m, "127.0.0.1:53")
|
||||
if err != nil {
|
||||
return APIErrorResponse{
|
||||
status: http.StatusInternalServerError,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
response := <-c
|
||||
var rrs []string
|
||||
|
||||
for _, rr := range response.RR {
|
||||
rrs = append(rrs, rr.String())
|
||||
}
|
||||
|
||||
if len(rrs) > 0 {
|
||||
rrs = rrs[0:len(rrs)-1]
|
||||
}
|
||||
|
||||
return APIResponse{
|
||||
response: map[string][]string{
|
||||
"rr": rrs,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type uploadedRR struct {
|
||||
RR string `json:"rr"`
|
||||
}
|
||||
|
||||
func addRR(zone string, body io.Reader) (Response) {
|
||||
var urr uploadedRR
|
||||
err := json.NewDecoder(body).Decode(&urr)
|
||||
if err != nil {
|
||||
return APIErrorResponse{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
rr, err := dns.NewRR(urr.RR)
|
||||
if err != nil {
|
||||
return APIErrorResponse{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.Id = dns.Id()
|
||||
m.Opcode = dns.OpcodeUpdate
|
||||
m.Question = make([]dns.Question, 1)
|
||||
m.Question[0] = dns.Question{zone, dns.TypeSOA, dns.ClassINET}
|
||||
|
||||
m.Insert([]dns.RR{rr})
|
||||
|
||||
c := new(dns.Client)
|
||||
c.TsigSecret = map[string]string{"ddns.": "so6ZGir4GPAqINNh9U5c3A=="}
|
||||
m.SetTsig("ddns.", dns.HmacSHA256, 300, time.Now().Unix())
|
||||
|
||||
in, rtt, err := c.Exchange(m, "127.0.0.1:53")
|
||||
if err != nil {
|
||||
return APIErrorResponse{
|
||||
status: http.StatusInternalServerError,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return APIResponse{
|
||||
response: map[string]interface{}{
|
||||
"in": *in,
|
||||
"rtt": rtt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func delRR(zone string, body io.Reader) (Response) {
|
||||
var urr uploadedRR
|
||||
err := json.NewDecoder(body).Decode(&urr)
|
||||
if err != nil {
|
||||
return APIErrorResponse{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
rr, err := dns.NewRR(urr.RR)
|
||||
if err != nil {
|
||||
return APIErrorResponse{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.Id = dns.Id()
|
||||
m.Opcode = dns.OpcodeUpdate
|
||||
m.Question = make([]dns.Question, 1)
|
||||
m.Question[0] = dns.Question{zone, dns.TypeSOA, dns.ClassINET}
|
||||
|
||||
m.Remove([]dns.RR{rr})
|
||||
|
||||
c := new(dns.Client)
|
||||
c.TsigSecret = map[string]string{"ddns.": "so6ZGir4GPAqINNh9U5c3A=="}
|
||||
m.SetTsig("ddns.", dns.HmacSHA256, 300, time.Now().Unix())
|
||||
|
||||
in, rtt, err := c.Exchange(m, "127.0.0.1:53")
|
||||
if err != nil {
|
||||
return APIErrorResponse{
|
||||
status: http.StatusInternalServerError,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return APIResponse{
|
||||
response: map[string]interface{}{
|
||||
"in": *in,
|
||||
"rtt": rtt,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package main
|
||||
|
||||
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 := "libredns"
|
||||
db_password := "libredns"
|
||||
db_host := ""
|
||||
db_db := "libredns"
|
||||
|
||||
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,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
password BINARY(64) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL
|
||||
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
|
||||
`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS zones(
|
||||
id_zone INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||
domain VARCHAR(255) NOT NULL,
|
||||
server VARCHAR(255),
|
||||
key_name VARCHAR(255) NOT NULL,
|
||||
key_blob BLOB NOT NULL
|
||||
) 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...)
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.nemunai.re/libredns/api"
|
||||
)
|
||||
|
||||
var DefaultNameServer = "127.0.0.1:53"
|
||||
|
||||
type ResponseWriterPrefix struct {
|
||||
real http.ResponseWriter
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (r ResponseWriterPrefix) Header() http.Header {
|
||||
return r.real.Header()
|
||||
}
|
||||
|
||||
func (r ResponseWriterPrefix) WriteHeader(s int) {
|
||||
if v, exists := r.real.Header()["Location"]; exists {
|
||||
r.real.Header().Set("Location", r.prefix+v[0])
|
||||
}
|
||||
r.real.WriteHeader(s)
|
||||
}
|
||||
|
||||
func (r ResponseWriterPrefix) Write(z []byte) (int, error) {
|
||||
return r.real.Write(z)
|
||||
}
|
||||
|
||||
func StripPrefix(prefix string, h http.Handler) http.Handler {
|
||||
if prefix == "" {
|
||||
return h
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if prefix != "/" && r.URL.Path == "/" {
|
||||
http.Redirect(w, r, prefix+"/", http.StatusFound)
|
||||
} else if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) {
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
r2.URL = new(url.URL)
|
||||
*r2.URL = *r.URL
|
||||
r2.URL.Path = p
|
||||
h.ServeHTTP(ResponseWriterPrefix{w, prefix}, r2)
|
||||
} else {
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Read parameters from command line
|
||||
var bind = flag.String("bind", ":8081", "Bind port/socket")
|
||||
var dsn = flag.String("dsn", DSNGenerator(), "DSN to connect to the MySQL server")
|
||||
var baseURL = flag.String("baseurl", "/", "URL prepended to each URL")
|
||||
flag.StringVar(&DefaultNameServer, "defaultns", DefaultNameServer, "Adress to the default name server")
|
||||
flag.Parse()
|
||||
|
||||
// Sanitize options
|
||||
if *baseURL != "/" {
|
||||
tmp := path.Clean(*baseURL)
|
||||
baseURL = &tmp
|
||||
} else {
|
||||
tmp := ""
|
||||
baseURL = &tmp
|
||||
}
|
||||
|
||||
// Initialize contents
|
||||
log.Println("Opening database...")
|
||||
if err := DBInit(*dsn); err != nil {
|
||||
log.Fatal("Cannot open the database: ", err)
|
||||
}
|
||||
defer DBClose()
|
||||
|
||||
log.Println("Creating database...")
|
||||
if err := DBCreate(); err != nil {
|
||||
log.Fatal("Cannot create database: ", err)
|
||||
}
|
||||
|
||||
// Serve content
|
||||
log.Println("Ready, listening on", *bind)
|
||||
if err := http.ListenAndServe(*bind, StripPrefix(*baseURL, api.Router())); err != nil {
|
||||
log.Fatal("Unable to listen and serve: ", err)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue