orchid/servers/api.go

257 lines
7.6 KiB
Go
Raw Permalink Normal View History

2023-07-06 01:26:17 +01:00
package servers
import (
"context"
2023-07-10 17:51:14 +01:00
"database/sql"
_ "embed"
2023-07-10 17:51:14 +01:00
"encoding/json"
"fmt"
"github.com/1f349/mjwt"
2024-08-12 22:27:23 +01:00
"github.com/1f349/mjwt/auth"
"github.com/1f349/orchid/database"
2024-05-13 19:04:43 +01:00
"github.com/1f349/orchid/logger"
2023-07-22 01:39:39 +01:00
oUtils "github.com/1f349/orchid/utils"
vUtils "github.com/1f349/violet/utils"
2023-07-06 01:26:17 +01:00
"github.com/julienschmidt/httprouter"
"net/http"
2024-06-23 12:48:27 +01:00
"slices"
2023-07-10 17:51:14 +01:00
"strconv"
2023-07-06 01:26:17 +01:00
"time"
)
2023-07-10 17:51:14 +01:00
type DomainStateValue struct {
Domain string `json:"domain"`
State int `json:"state"`
}
type Certificate struct {
Id int64 `json:"id"`
AutoRenew bool `json:"auto_renew"`
Active bool `json:"active"`
Renewing bool `json:"renewing"`
RenewFailed bool `json:"renew_failed"`
NotAfter time.Time `json:"not_after"`
UpdatedAt time.Time `json:"updated_at"`
2023-11-14 14:00:58 +00:00
Domains []string `json:"domains"`
}
2023-07-06 01:26:17 +01:00
// NewApiServer creates and runs a http server containing all the API
// endpoints for the software
//
// `/cert` - edit certificate
2024-08-12 22:27:23 +01:00
func NewApiServer(listen string, db *database.Queries, signer *mjwt.KeyStore, domains oUtils.DomainChecker) *http.Server {
2023-07-06 01:26:17 +01:00
r := httprouter.New()
2023-10-17 00:11:15 +01:00
r.GET("/", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
http.Error(rw, "Orchid API Endpoint", http.StatusOK)
})
// Endpoint for grabbing owned certificates
2024-06-23 12:48:27 +01:00
// TODO(melon): rewrite this endpoint to prevent using a map then converting into a slice later
r.GET("/owned", checkAuthWithPerm(signer, "orchid:cert", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
domains := getDomainOwnershipClaims(b.Claims.Perms)
domainMap := make(map[string]bool)
for _, i := range domains {
domainMap[i] = true
}
// query database
rows, err := db.FindOwnedCerts(context.Background())
if err != nil {
2024-06-04 18:26:07 +01:00
logger.Logger.Info("Failed after reading certificates from database:", "err", err)
http.Error(rw, "Database Error", http.StatusInternalServerError)
return
}
mOther := make(map[int64]*Certificate) // other certificates
m := make(map[int64]*Certificate) // certificates owned by this user
// loop over query rows
for _, row := range rows {
c := Certificate{
Id: row.ID,
AutoRenew: row.AutoRenew,
Active: row.Active,
Renewing: row.Renewing,
RenewFailed: row.RenewFailed,
NotAfter: row.NotAfter,
UpdatedAt: row.UpdatedAt,
}
d := row.Domain
// check in owned map
if cert, ok := m[c.Id]; ok {
cert.Domains = append(cert.Domains, d)
continue
}
// get etld+1
topFqdn, found := vUtils.GetTopFqdn(d)
if !found {
2024-06-04 18:26:07 +01:00
logger.Logger.Info("Invalid domain found:", "domain", d)
http.Error(rw, "Database Error", http.StatusInternalServerError)
return
}
// if found in other, add domain and put in main if owned
if cert, ok := mOther[c.Id]; ok {
cert.Domains = append(cert.Domains, d)
if domainMap[topFqdn] {
m[c.Id] = cert
}
continue
}
// add to other and main if owned
c.Domains = []string{d}
mOther[c.Id] = &c
if domainMap[topFqdn] {
m[c.Id] = &c
}
}
2024-06-23 12:48:27 +01:00
// remap into a slice
arr := make([]*Certificate, 0, len(m))
slices.SortFunc(arr, func(a, b *Certificate) int {
return int(a.Id - b.Id)
})
for _, v := range m {
arr = append(arr, v)
}
rw.WriteHeader(http.StatusOK)
2024-06-23 12:48:27 +01:00
_ = json.NewEncoder(rw).Encode(arr)
}))
2023-07-10 17:51:14 +01:00
// Endpoint for looking up a certificate
r.GET("/lookup/:domain", checkAuthWithPerm(signer, "orchid:cert", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
2023-07-06 01:26:17 +01:00
domain := params.ByName("domain")
if !domains.ValidateDomain(domain) {
vUtils.RespondVioletError(rw, http.StatusBadRequest, "Invalid domain")
return
}
2023-07-10 17:51:14 +01:00
}))
r.POST("/cert", checkAuthWithPerm(signer, "orchid:cert", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
err := db.AddCertificate(req.Context(), database.AddCertificateParams{
Owner: b.Subject,
Dns: sql.NullInt64{},
NotAfter: time.Now(),
UpdatedAt: time.Now(),
})
2023-07-10 17:51:14 +01:00
if err != nil {
apiError(rw, http.StatusInternalServerError, "Failed to delete certificate")
return
}
2023-07-06 01:26:17 +01:00
rw.WriteHeader(http.StatusAccepted)
2023-07-10 17:51:14 +01:00
}))
r.DELETE("/cert/:id", checkAuthForCertificate(signer, "orchid:cert", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId int64) {
err := db.RemoveCertificate(req.Context(), certId)
2023-07-10 17:51:14 +01:00
if err != nil {
apiError(rw, http.StatusInternalServerError, "Failed to delete certificate")
return
}
rw.WriteHeader(http.StatusAccepted)
}))
// Endpoint for adding/removing domains to/from a certificate
managePutDelete := certDomainManagePUTandDELETE(db, signer, domains)
r.GET("/cert/:id/domains", certDomainManageGET(db, signer))
2023-07-10 17:51:14 +01:00
r.PUT("/cert/:id/domains", managePutDelete)
r.DELETE("/cert/:id/domains", managePutDelete)
// Endpoint for generating a temporary certificate for modified domains
r.POST("/cert/:id/temp", checkAuth(signer, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
if !b.Claims.Perms.Has("orchid:cert") {
2023-07-10 17:51:14 +01:00
apiError(rw, http.StatusForbidden, "No permission")
return
}
// lookup certificate owner
id, err := checkCertOwner(db, "", b)
if err != nil {
apiError(rw, http.StatusInsufficientStorage, "Database error")
return
}
// run a safe transaction to create the temporary certificate
2024-03-09 00:55:06 +00:00
if db.UseTx(req.Context(), func(tx *database.Queries) error {
2023-07-10 17:51:14 +01:00
// insert temporary certificate into database
2024-03-09 00:55:06 +00:00
err := tx.AddTempCertificate(req.Context(), database.AddTempCertificateParams{
Owner: b.Subject,
UpdatedAt: time.Now(),
TempParent: sql.NullInt64{Valid: true, Int64: id},
})
2023-07-10 17:51:14 +01:00
return err
}) != nil {
apiError(rw, http.StatusInsufficientStorage, "Database error")
fmt.Printf("Internal error: %s\n", err)
return
}
}))
2023-07-06 01:26:17 +01:00
// Create and run http server
return &http.Server{
Addr: listen,
Handler: r,
ReadTimeout: time.Minute,
ReadHeaderTimeout: time.Minute,
WriteTimeout: time.Minute,
IdleTimeout: time.Minute,
MaxHeaderBytes: 2500,
}
}
2023-07-10 17:51:14 +01:00
// apiError outputs a generic JSON error message
func apiError(rw http.ResponseWriter, code int, m string) {
rw.WriteHeader(code)
_ = json.NewEncoder(rw).Encode(map[string]string{
"error": m,
})
}
// lookupCertOwner finds the certificate matching the id string and returns the
// numeric id, owner and possible error, only works for active certificates.
func checkCertOwner(db *database.Queries, idStr string, b AuthClaims) (int64, error) {
2023-07-10 17:51:14 +01:00
// parse the id
rawId, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return 0, err
}
// run database query
row, err := db.CheckCertOwner(context.Background(), int64(rawId))
2023-07-10 17:51:14 +01:00
if err != nil {
return 0, err
2023-07-10 17:51:14 +01:00
}
// check the owner is the mjwt token subject
if b.Subject != row.Owner {
return row.ID, fmt.Errorf("not the certificate owner")
2023-07-10 17:51:14 +01:00
}
// it's all valid, return the values
return row.ID, nil
2023-07-10 17:51:14 +01:00
}
// getDomainOwnershipClaims returns the domains marked as owned from PermStorage,
// they match `domain:owns=<fqdn>` where fqdn will be returned
2024-08-12 22:27:23 +01:00
func getDomainOwnershipClaims(perms *auth.PermStorage) []string {
a := perms.Search("domain:owns=*")
for i := range a {
a[i] = a[i][len("domain:owns="):]
}
return a
}
// validateDomainOwnershipClaims validates if the claims contain the
// `domain:owns=<fqdn>` field with the matching top level domain
2024-08-12 22:27:23 +01:00
func validateDomainOwnershipClaims(a string, perms *auth.PermStorage) bool {
2023-07-10 17:51:14 +01:00
if fqdn, ok := vUtils.GetTopFqdn(a); ok {
if perms.Has("domain:owns=" + fqdn) {
return true
2023-07-10 17:51:14 +01:00
}
}
return false
2023-07-06 01:26:17 +01:00
}