Checkpoint
This commit is contained in:
146
admin/admin.go
Normal file
146
admin/admin.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"git.maze.io/maze/styx/dataset"
|
||||
"git.maze.io/maze/styx/logger"
|
||||
"git.maze.io/maze/styx/proxy"
|
||||
)
|
||||
|
||||
type Admin struct {
|
||||
Storage dataset.Storage
|
||||
setupOnce sync.Once
|
||||
mux *http.ServeMux
|
||||
api *http.ServeMux
|
||||
}
|
||||
|
||||
type apiError struct {
|
||||
Code int
|
||||
Err error
|
||||
}
|
||||
|
||||
func (err apiError) Error() string {
|
||||
return err.Err.Error()
|
||||
}
|
||||
|
||||
func (a *Admin) setup() {
|
||||
a.mux = http.NewServeMux()
|
||||
|
||||
a.api = http.NewServeMux()
|
||||
a.api.HandleFunc("GET /groups", a.apiGroups)
|
||||
a.api.HandleFunc("POST /group", a.apiGroupCreate)
|
||||
a.api.HandleFunc("GET /group/{id}", a.apiGroup)
|
||||
a.api.HandleFunc("PATCH /group/{id}", a.apiGroupUpdate)
|
||||
a.api.HandleFunc("DELETE /group/{id}", a.apiGroupDelete)
|
||||
a.api.HandleFunc("GET /clients", a.apiClients)
|
||||
a.api.HandleFunc("GET /client/{id}", a.apiClient)
|
||||
a.api.HandleFunc("POST /client", a.apiClientCreate)
|
||||
a.api.HandleFunc("PATCH /client/{id}", a.apiClientUpdate)
|
||||
a.api.HandleFunc("DELETE /client/{id}", a.apiClientDelete)
|
||||
a.api.HandleFunc("GET /lists", a.apiLists)
|
||||
a.api.HandleFunc("POST /list", a.apiListCreate)
|
||||
a.api.HandleFunc("GET /list/{id}", a.apiList)
|
||||
a.api.HandleFunc("DELETE /list/{id}", a.apiListDelete)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
Handle(pattern string, handler http.Handler)
|
||||
}
|
||||
|
||||
func (a *Admin) Install(handler Handler) {
|
||||
a.setupOnce.Do(a.setup)
|
||||
handler.Handle("/api/v1/", http.StripPrefix("/api/v1", a.api))
|
||||
}
|
||||
|
||||
func (a *Admin) handleAPIError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
code := http.StatusBadRequest
|
||||
switch {
|
||||
case dataset.IsNotExist(err):
|
||||
code = http.StatusNotFound
|
||||
case os.IsPermission(err):
|
||||
code = http.StatusForbidden
|
||||
case errors.Is(err, apiError{}):
|
||||
if c := err.(apiError).Code; c > 0 {
|
||||
code = c
|
||||
}
|
||||
}
|
||||
|
||||
logger.StandardLog.Err(err).Values(logger.Values{
|
||||
"code": code,
|
||||
"client": r.RemoteAddr,
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
}).Warn("Unexpected API error encountered")
|
||||
|
||||
var data []byte
|
||||
if err, ok := err.(apiError); ok {
|
||||
data, _ = json.Marshal(struct {
|
||||
Code int `json:"code"`
|
||||
Error string `json:"error"`
|
||||
}{code, err.Error()})
|
||||
} else {
|
||||
data, _ = json.Marshal(struct {
|
||||
Code int `json:"code"`
|
||||
Error string `json:"error"`
|
||||
}{code, http.StatusText(code)})
|
||||
}
|
||||
|
||||
res := proxy.NewResponse(code, io.NopCloser(bytes.NewReader(data)), r)
|
||||
res.Header.Set(proxy.HeaderContentType, "application/json")
|
||||
|
||||
for k, vv := range res.Header {
|
||||
if len(vv) >= 1 {
|
||||
w.Header().Set(k, vv[0])
|
||||
for _, v := range vv[1:] {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
io.Copy(w, res.Body)
|
||||
}
|
||||
|
||||
func (a *Admin) jsonResponse(w http.ResponseWriter, r *http.Request, value any, codes ...int) {
|
||||
var (
|
||||
code = http.StatusNoContent
|
||||
body io.ReadCloser
|
||||
size int64
|
||||
)
|
||||
if value != nil {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
code = http.StatusOK
|
||||
body = io.NopCloser(bytes.NewReader(data))
|
||||
size = int64(len(data))
|
||||
}
|
||||
if len(codes) > 0 {
|
||||
code = codes[0]
|
||||
}
|
||||
|
||||
res := proxy.NewResponse(code, body, r)
|
||||
res.Close = true
|
||||
res.Header.Set(proxy.HeaderContentLength, strconv.FormatInt(size, 10))
|
||||
res.Header.Set(proxy.HeaderContentType, "application/json")
|
||||
|
||||
for k, vv := range res.Header {
|
||||
if len(vv) >= 1 {
|
||||
w.Header().Set(k, vv[0])
|
||||
for _, v := range vv[1:] {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
io.Copy(w, res.Body)
|
||||
}
|
183
admin/api_client.go
Normal file
183
admin/api_client.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.maze.io/maze/styx/dataset"
|
||||
)
|
||||
|
||||
func (a *Admin) apiClients(w http.ResponseWriter, r *http.Request) {
|
||||
clients, err := a.Storage.Clients()
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.jsonResponse(w, r, clients)
|
||||
}
|
||||
|
||||
func (a *Admin) apiClient(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
client, err := a.Storage.ClientByID(id)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.jsonResponse(w, r, client)
|
||||
}
|
||||
|
||||
func (a *Admin) apiClientCreate(w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
dataset.Client
|
||||
Groups []int64 `json:"groups"`
|
||||
ID int64 `json:"id"` // mask, not used
|
||||
CreatedAt time.Time `json:"created_at"` // mask, not used
|
||||
UpdatedAt time.Time `json:"updated_at"` // mask, not used
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.verifyClient(&request.Client); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var groups []dataset.Group
|
||||
for _, id := range request.Groups {
|
||||
group, err := a.Storage.GroupByID(id)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
|
||||
request.Client.Groups = groups
|
||||
if err := a.Storage.SaveClient(&request.Client); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
a.jsonResponse(w, r, request.Client)
|
||||
}
|
||||
|
||||
func (a *Admin) apiClientUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := a.Storage.ClientByID(id)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
log.Printf("updating: %#+v", client)
|
||||
|
||||
var request struct {
|
||||
dataset.Client
|
||||
Groups []int64 `json:"groups"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.verifyClient(&request.Client); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
client.IP = request.Client.IP
|
||||
client.Mask = request.Client.Mask
|
||||
client.Description = request.Client.Description
|
||||
client.Groups = client.Groups[:0]
|
||||
for _, id := range request.Groups {
|
||||
group, err := a.Storage.GroupByID(id)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
client.Groups = append(client.Groups, group)
|
||||
}
|
||||
if err := a.Storage.SaveClient(&client); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
a.jsonResponse(w, r, client)
|
||||
}
|
||||
|
||||
func (a *Admin) apiClientDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
client, err := a.Storage.ClientByID(id)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = a.Storage.DeleteClient(client); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.jsonResponse(w, r, nil)
|
||||
}
|
||||
|
||||
func (a *Admin) verifyClient(c *dataset.Client) (err error) {
|
||||
ip := net.ParseIP(c.IP)
|
||||
switch c.Network {
|
||||
case "ipv4":
|
||||
if ip.To4() == nil {
|
||||
return apiError{Err: errors.New("invalid IPv4 address")}
|
||||
}
|
||||
if c.Mask == 0 {
|
||||
c.Mask = 32 // one IP
|
||||
}
|
||||
if c.Mask <= 0 || c.Mask > 32 {
|
||||
return apiError{Err: errors.New("mask can't be zero")}
|
||||
}
|
||||
c.IP = ip.Mask(net.CIDRMask(int(c.Mask), 32)).String()
|
||||
|
||||
case "ipv6":
|
||||
if ip.To16() == nil {
|
||||
return apiError{Err: errors.New("invalid IPv6 address")}
|
||||
}
|
||||
if c.Mask == 0 {
|
||||
c.Mask = 128 // one IP
|
||||
}
|
||||
if c.Mask <= 0 || c.Mask > 128 {
|
||||
return apiError{Err: errors.New("mask can't be zero")}
|
||||
}
|
||||
c.IP = ip.Mask(net.CIDRMask(int(c.Mask), 128)).String()
|
||||
|
||||
case "":
|
||||
if ip.To4() != nil {
|
||||
c.Network = "ipv4"
|
||||
} else if ip.To16() != nil {
|
||||
c.Network = "ipv6"
|
||||
} else {
|
||||
return apiError{Err: errors.New("invalid IP address")}
|
||||
}
|
||||
return a.verifyClient(c)
|
||||
|
||||
default:
|
||||
return apiError{Err: fmt.Errorf("invalid network %q", c.Network)}
|
||||
}
|
||||
return
|
||||
}
|
72
admin/api_group.go
Normal file
72
admin/api_group.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.maze.io/maze/styx/dataset"
|
||||
)
|
||||
|
||||
func (a *Admin) apiGroups(w http.ResponseWriter, r *http.Request) {
|
||||
groups, err := a.Storage.Groups()
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.jsonResponse(w, r, groups)
|
||||
}
|
||||
|
||||
func (a *Admin) apiGroup(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
group, err := a.Storage.GroupByID(id)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.jsonResponse(w, r, group)
|
||||
}
|
||||
|
||||
func (a *Admin) apiGroupCreate(w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
dataset.Group
|
||||
ID int64 `json:"id"` // mask, not used
|
||||
CreatedAt time.Time `json:"created_at"` // mask, not used
|
||||
UpdatedAt time.Time `json:"updated_at"` // mask, not used
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err := a.Storage.SaveGroup(&request.Group); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.jsonResponse(w, r, request.Group, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (a *Admin) apiGroupUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (a *Admin) apiGroupDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
group, err := a.Storage.GroupByID(id)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = a.Storage.DeleteGroup(group); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.jsonResponse(w, r, nil)
|
||||
}
|
98
admin/api_list.go
Normal file
98
admin/api_list.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.maze.io/maze/styx/dataset"
|
||||
)
|
||||
|
||||
func (a *Admin) apiLists(w http.ResponseWriter, r *http.Request) {
|
||||
lists, err := a.Storage.Lists()
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.jsonResponse(w, r, lists)
|
||||
}
|
||||
|
||||
func (a *Admin) apiList(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
list, err := a.Storage.ListByID(id)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.jsonResponse(w, r, list)
|
||||
}
|
||||
|
||||
func (a *Admin) apiListCreate(w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
dataset.List
|
||||
Groups []int64 `json:"groups"`
|
||||
ID int64 `json:"id"` // mask, not used
|
||||
CreatedAt time.Time `json:"created_at"` // mask, not used
|
||||
UpdatedAt time.Time `json:"updated_at"` // mask, not used
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.verifyList(&request.List); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
request.List.Groups = request.List.Groups[:0]
|
||||
for _, id := range request.Groups {
|
||||
group, err := a.Storage.GroupByID(id)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
request.List.Groups = append(request.List.Groups, group)
|
||||
}
|
||||
|
||||
if err := a.Storage.SaveList(&request.List); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
a.jsonResponse(w, r, request.List)
|
||||
}
|
||||
|
||||
func (a *Admin) apiListDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
list, err := a.Storage.ListByID(id)
|
||||
if err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = a.Storage.DeleteList(list); err != nil {
|
||||
a.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.jsonResponse(w, r, nil)
|
||||
}
|
||||
|
||||
func (a *Admin) verifyList(list *dataset.List) error {
|
||||
switch list.Type {
|
||||
case dataset.ListTypeDomain, dataset.ListTypeNetwork:
|
||||
default:
|
||||
return apiError{Err: fmt.Errorf("unknown list type %q", list.Type)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user