Checkpoint

This commit is contained in:
2025-10-06 22:25:23 +02:00
parent a23259cfdc
commit a254b306f2
48 changed files with 3327 additions and 212 deletions

146
admin/admin.go Normal file
View 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
View 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
View 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
View 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
}