2023-07-06 01:26:17 +01:00
|
|
|
package servers
|
|
|
|
|
|
|
|
import (
|
2023-07-10 17:51:14 +01:00
|
|
|
"database/sql"
|
2023-11-14 13:24:30 +00:00
|
|
|
_ "embed"
|
2023-07-10 17:51:14 +01:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2023-11-14 13:24:30 +00:00
|
|
|
"github.com/1f349/mjwt"
|
|
|
|
"github.com/1f349/mjwt/claims"
|
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"
|
2023-11-14 13:24:30 +00:00
|
|
|
"log"
|
2023-07-06 01:26:17 +01:00
|
|
|
"net/http"
|
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"`
|
|
|
|
}
|
|
|
|
|
2023-11-14 13:24:30 +00:00
|
|
|
type Certificate struct {
|
|
|
|
Id int `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-11-14 13:24:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//go:embed find-owned-certs.sql
|
|
|
|
var findOwnedCerts string
|
|
|
|
|
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
|
2023-07-10 17:51:14 +01:00
|
|
|
func NewApiServer(listen string, db *sql.DB, signer mjwt.Verifier, 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)
|
|
|
|
})
|
|
|
|
|
2023-11-14 13:24:30 +00:00
|
|
|
// Endpoint for grabbing owned certificates
|
|
|
|
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
|
|
|
|
query, err := db.Query(findOwnedCerts)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, "Database Error", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
mOther := make(map[int]*Certificate) // other certificates
|
|
|
|
m := make(map[int]*Certificate) // certificates owned by this user
|
|
|
|
|
|
|
|
// loop over query rows
|
|
|
|
for query.Next() {
|
|
|
|
var c Certificate
|
|
|
|
var d string
|
|
|
|
err := query.Scan(&c.Id, &c.AutoRenew, &c.Active, &c.Renewing, &c.RenewFailed, &c.NotAfter, &c.UpdatedAt, &d)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("Failed to read certificate from database: ", err)
|
|
|
|
http.Error(rw, "Database Error", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
log.Println("Invalid domain found: ", 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
|
|
|
|
}
|
2023-11-14 13:40:14 +00:00
|
|
|
continue
|
2023-11-14 13:24:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// add to other and main if owned
|
|
|
|
c.Domains = []string{d}
|
|
|
|
mOther[c.Id] = &c
|
|
|
|
if domainMap[topFqdn] {
|
|
|
|
m[c.Id] = &c
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := query.Err(); err != nil {
|
|
|
|
log.Println("Failed after reading certificates from database: ", err)
|
|
|
|
http.Error(rw, "Database Error", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
rw.WriteHeader(http.StatusOK)
|
|
|
|
_ = json.NewEncoder(rw).Encode(m)
|
|
|
|
}))
|
|
|
|
|
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
|
|
|
}))
|
|
|
|
|
2023-11-14 13:24:30 +00:00
|
|
|
r.POST("/cert", checkAuthWithPerm(signer, "orchid:cert", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
|
2023-07-10 17:51:14 +01:00
|
|
|
_, err := db.Exec(`INSERT INTO certificates (owner, dns, updated_at) VALUES (?, ?, ?)`, b.Subject, 0, time.Now())
|
|
|
|
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
|
|
|
}))
|
2023-11-14 13:24:30 +00:00
|
|
|
r.DELETE("/cert/:id", checkAuthForCertificate(signer, "orchid:cert", db, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, certId uint64) {
|
2023-07-10 17:51:14 +01:00
|
|
|
_, err := db.Exec(`UPDATE certificates SET active = 0 WHERE id = ?`, certId)
|
|
|
|
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
|
2023-07-12 20:55:53 +01:00
|
|
|
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) {
|
2023-11-14 13:24:30 +00:00
|
|
|
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
|
|
|
|
if safeTransaction(rw, db, func(rw http.ResponseWriter, tx *sql.Tx) error {
|
|
|
|
// insert temporary certificate into database
|
|
|
|
_, err := db.Exec(`INSERT INTO certificates (owner, dns, active, updated_at, temp_parent) VALUES (?, 0, 1, ?, ?)`, b.Subject, time.Now(), id)
|
|
|
|
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 *sql.DB, idStr string, b AuthClaims) (uint64, error) {
|
|
|
|
// parse the id
|
|
|
|
rawId, err := strconv.ParseUint(idStr, 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// run database query
|
|
|
|
row := db.QueryRow(`SELECT id, owner FROM certificates WHERE active = 1 and id = ?`, rawId)
|
|
|
|
|
|
|
|
// scan in result values
|
|
|
|
var id uint64
|
|
|
|
var owner string
|
|
|
|
err = row.Scan(&id, &owner)
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("scan error: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// check the owner is the mjwt token subject
|
|
|
|
if b.Subject != owner {
|
|
|
|
return id, fmt.Errorf("not the certificate owner")
|
|
|
|
}
|
|
|
|
|
|
|
|
// it's all valid, return the values
|
|
|
|
return id, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// safeTransaction completes a database transaction safely allowing for rollbacks
|
|
|
|
// if the callback errors
|
|
|
|
func safeTransaction(rw http.ResponseWriter, db *sql.DB, cb func(rw http.ResponseWriter, tx *sql.Tx) error) error {
|
|
|
|
// start a transaction
|
|
|
|
begin, err := db.Begin()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to begin a transaction")
|
2023-07-06 01:26:17 +01:00
|
|
|
}
|
|
|
|
|
2023-07-10 17:51:14 +01:00
|
|
|
// init defer rollback
|
|
|
|
needsRollback := true
|
|
|
|
defer func() {
|
|
|
|
if needsRollback {
|
|
|
|
_ = begin.Rollback()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
// run main code within the transaction session
|
|
|
|
err = cb(rw, begin)
|
2023-07-06 01:26:17 +01:00
|
|
|
if err != nil {
|
2023-07-10 17:51:14 +01:00
|
|
|
return err
|
2023-07-06 01:26:17 +01:00
|
|
|
}
|
|
|
|
|
2023-07-10 17:51:14 +01:00
|
|
|
// clear the rollback flag and commit the transaction
|
|
|
|
needsRollback = false
|
|
|
|
if begin.Commit() != nil {
|
|
|
|
return fmt.Errorf("failed to commit a transaction")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-11-14 13:24:30 +00:00
|
|
|
// getDomainOwnershipClaims returns the domains marked as owned from PermStorage,
|
|
|
|
// they match `domain:owns=<fqdn>` where fqdn will be returned
|
|
|
|
func getDomainOwnershipClaims(perms *claims.PermStorage) []string {
|
|
|
|
a := perms.Search("domain:owns=*")
|
|
|
|
for i := range a {
|
|
|
|
a[i] = a[i][len("domain:owns="):]
|
|
|
|
}
|
|
|
|
return a
|
|
|
|
}
|
|
|
|
|
2023-07-12 20:55:53 +01:00
|
|
|
// validateDomainOwnershipClaims validates if the claims contain the
|
2023-11-14 13:24:30 +00:00
|
|
|
// `domain:owns=<fqdn>` field with the matching top level domain
|
2023-07-12 20:55:53 +01:00
|
|
|
func validateDomainOwnershipClaims(a string, perms *claims.PermStorage) bool {
|
2023-07-10 17:51:14 +01:00
|
|
|
if fqdn, ok := vUtils.GetTopFqdn(a); ok {
|
2023-11-14 13:24:30 +00:00
|
|
|
if perms.Has("domain:owns=" + fqdn) {
|
2023-07-12 20:55:53 +01:00
|
|
|
return true
|
2023-07-10 17:51:14 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
2023-07-06 01:26:17 +01:00
|
|
|
}
|