|
|
//LEAPI - ACME Certificate Renewal Control API - Copyright 2022 Ruel Tmeizeh All Rights Reserved
|
|
|
|
|
|
package main
|
|
|
|
|
|
import (
|
|
|
"bytes"
|
|
|
"crypto/tls"
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
"io"
|
|
|
"io/ioutil"
|
|
|
"log"
|
|
|
"net/http"
|
|
|
"os"
|
|
|
"os/exec"
|
|
|
"os/signal"
|
|
|
"path"
|
|
|
"reflect"
|
|
|
"regexp"
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
"sync"
|
|
|
"syscall"
|
|
|
"time"
|
|
|
|
|
|
"github.com/fatih/color"
|
|
|
"github.com/google/uuid"
|
|
|
"github.com/labstack/echo/v4"
|
|
|
"github.com/labstack/echo/v4/middleware"
|
|
|
)
|
|
|
|
|
|
const version string = "1.1.0"
|
|
|
const serverVersion string = "RuhNet LE API v" + version
|
|
|
const apiVersion int = 1
|
|
|
const website string = "https://ruhnet.co"
|
|
|
const myUserAgent string = "RuhNet LE Cluster API Controller"
|
|
|
|
|
|
const timeout time.Duration = time.Duration(30 * time.Second) //Timeout for outbound requests. Adjust as needed.
|
|
|
|
|
|
var leapiconf LEAPIConfig
|
|
|
|
|
|
var startupTime time.Time
|
|
|
var configDir string
|
|
|
var domains []string
|
|
|
var servers []string
|
|
|
var syncScheme string = "http://"
|
|
|
var syncPort string
|
|
|
|
|
|
const banner = `
|
|
|
____ __ _ _ __
|
|
|
/ ___\ __ __/ /_ / \ / /__ __/ /_
|
|
|
/ /_/ // /_/ / _ \/ / \/ //__\_ __/
|
|
|
/_/ \_\\ ___/_/ /_/_/ \__/ \__,/_/ %s
|
|
|
_____________________________________________________
|
|
|
`
|
|
|
|
|
|
//////////////////////////
|
|
|
//Data Structs:
|
|
|
|
|
|
type LEAPIConfig struct {
|
|
|
Hostname string `json:"hostname"`
|
|
|
SyncType string `json:"sync_type"`
|
|
|
Username string `json:"username"`
|
|
|
SrvDir string `json:"srv_dir"`
|
|
|
LogFile string `json:"log_file"`
|
|
|
Debug bool `json:"debug"`
|
|
|
HTTP_ServerPort string `json:"http_server_port"`
|
|
|
HTTPS_ServerPort string `json:"https_server_port"`
|
|
|
TLSCertFile string `json:"tls_cert_path"`
|
|
|
TLSKeyFile string `json:"tls_key_path"`
|
|
|
TLSChainFile string `json:"tls_chain_path"`
|
|
|
TLSPEMFile string `json:"tls_pem_path"`
|
|
|
TLSCAFile string `json:"tls_ca_path"`
|
|
|
FrontEndURL string `json:"frontend_url"`
|
|
|
PrimaryDomain string `json:"primary_domain"`
|
|
|
LetsEncryptValidationPath string `json:"letsencrypt_validation_path"`
|
|
|
ReloadCommand string `json:"reload_command"`
|
|
|
RenewAllow string `json:"renew_allow_days"`
|
|
|
SecretKey string `json:"secret_key"`
|
|
|
Production bool `json:"production"`
|
|
|
CheckPort string `json:"check_port"`
|
|
|
}
|
|
|
|
|
|
type UpOut struct {
|
|
|
Up bool `json:"up,omitempty"`
|
|
|
StartTime time.Time `json:"start_time,omitempty"`
|
|
|
Uptime string `json:"uptime,omitempty"`
|
|
|
}
|
|
|
|
|
|
type APIOutput struct {
|
|
|
Status int `json:"status,omitempty"`
|
|
|
Message string `json:"message,omitempty"`
|
|
|
Data []string `json:"data,omitempty"`
|
|
|
}
|
|
|
|
|
|
type keypairReloader struct {
|
|
|
certMu sync.RWMutex
|
|
|
cert *tls.Certificate
|
|
|
certPath string
|
|
|
keyPath string
|
|
|
}
|
|
|
|
|
|
func main() {
|
|
|
/////////////////////////////////////////////
|
|
|
startupTime = time.Now()
|
|
|
|
|
|
//Read config:
|
|
|
configFilename := "leapi_config.json"
|
|
|
configDir = os.Getenv("LEAPI_CONFDIR")
|
|
|
if configDir == "" {
|
|
|
confDirs := []string{
|
|
|
"/usr/local/etc",
|
|
|
"/opt/leapi",
|
|
|
"/opt/leapi/etc",
|
|
|
"/var/lib/leapi",
|
|
|
"/etc",
|
|
|
}
|
|
|
configDir = "." //the fallback
|
|
|
for _, cd := range confDirs {
|
|
|
if _, err := os.Stat(cd + "/" + configFilename); os.IsNotExist(err) { //doesn't exist...
|
|
|
continue //..so check next one
|
|
|
}
|
|
|
configDir = cd
|
|
|
}
|
|
|
}
|
|
|
configFile := configDir + "/" + configFilename
|
|
|
jsonFile, err := os.Open(configFile)
|
|
|
if err != nil {
|
|
|
log.Fatal("Could not open config file: " + configFile + "\n" + err.Error())
|
|
|
}
|
|
|
defer jsonFile.Close()
|
|
|
fileBytes, _ := ioutil.ReadAll(jsonFile)
|
|
|
|
|
|
//strip out // comments from config file:
|
|
|
re := regexp.MustCompile(`([\s]//.*)|(^//.*)`)
|
|
|
fileCleanedBytes := re.ReplaceAll(fileBytes, nil)
|
|
|
|
|
|
err = json.Unmarshal(fileCleanedBytes, &leapiconf) //populate the config struct with JSON data from the config file
|
|
|
if err != nil {
|
|
|
log.Fatal("Could not parse config file: " + configFile + "\n" + err.Error())
|
|
|
}
|
|
|
|
|
|
leapiconf.checkConfig()
|
|
|
|
|
|
log.Println("Configuration OK, starting LEAPI...")
|
|
|
fmt.Println()
|
|
|
|
|
|
leapiLogFile, err := os.OpenFile(leapiconf.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
|
|
|
if err != nil {
|
|
|
log.Println("Could not open log file: " + leapiconf.LogFile + "\n" + err.Error())
|
|
|
leapiLogFile, err = os.OpenFile("/tmp/leapi.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
|
|
|
if err != nil {
|
|
|
log.Fatal("Can't open even /tmp log file!\n" + err.Error())
|
|
|
}
|
|
|
}
|
|
|
defer leapiLogFile.Close()
|
|
|
//set other logging to same file
|
|
|
log.SetOutput(leapiLogFile)
|
|
|
|
|
|
//Startup Banner
|
|
|
fmt.Printf(banner, website)
|
|
|
fmt.Println(serverVersion + "\n")
|
|
|
|
|
|
//read domains file
|
|
|
domainsFile := configDir + "/domains.json"
|
|
|
if fileExists(domainsFile) {
|
|
|
jsonFile, err = os.Open(domainsFile)
|
|
|
if err != nil {
|
|
|
log.Fatal("Could not open domains.json file: " + err.Error())
|
|
|
}
|
|
|
defer jsonFile.Close()
|
|
|
fileBytes, err := ioutil.ReadAll(jsonFile)
|
|
|
err = json.Unmarshal(fileBytes, &domains)
|
|
|
if err != nil {
|
|
|
log.Fatal("Could not parse domains.json file: " + err.Error())
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//read servers file
|
|
|
serversFile := configDir + "/servers.json"
|
|
|
if fileExists(serversFile) {
|
|
|
jsonFile, err = os.Open(serversFile)
|
|
|
if err != nil {
|
|
|
log.Fatal("Could not open servers.json file: " + err.Error())
|
|
|
}
|
|
|
defer jsonFile.Close()
|
|
|
fileBytes, err := ioutil.ReadAll(jsonFile)
|
|
|
err = json.Unmarshal(fileBytes, &servers)
|
|
|
if err != nil {
|
|
|
log.Fatal("Could not parse servers.json file: " + err.Error())
|
|
|
}
|
|
|
}
|
|
|
|
|
|
syncPort = leapiconf.HTTP_ServerPort
|
|
|
if leapiconf.LetsEncryptValidationPath == "-" {
|
|
|
leapiconf.LetsEncryptValidationPath = leapiconf.SrvDir + "/acme-challenge"
|
|
|
}
|
|
|
|
|
|
if leapiconf.Hostname == "-" {
|
|
|
hostname, err := os.Hostname()
|
|
|
if err != nil {
|
|
|
log.Fatal("Hostname could not be auto-detected from system: " + err.Error())
|
|
|
}
|
|
|
leapiconf.Hostname = hostname
|
|
|
}
|
|
|
fmt.Println("My hostname: " + leapiconf.Hostname)
|
|
|
|
|
|
/////////////////////////////////////////////
|
|
|
//Echo config:
|
|
|
e := echo.New() // Echo instance
|
|
|
|
|
|
e.HideBanner = true
|
|
|
e.Use(middleware.Recover())
|
|
|
//e.Logger.SetLevel(stdLog.DEBUG)
|
|
|
//e.Debug = true
|
|
|
//e.Use(middleware.Logger())
|
|
|
/*
|
|
|
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
|
Output: leapiLogFile,
|
|
|
}))
|
|
|
*/
|
|
|
|
|
|
/////////////////////////////////////////////
|
|
|
// ROUTE GROUPS
|
|
|
api := e.Group("/api") //API routes
|
|
|
apiFile := e.Group("/api/file") //API routes
|
|
|
/////////////////////////////////////////////
|
|
|
|
|
|
/////////////////////////////////////////////
|
|
|
// MIDDLEWARE
|
|
|
//Add server header and CORS
|
|
|
e.Use(serverHeaders)
|
|
|
//Auth API routes
|
|
|
api.Use(middleware.KeyAuth(apiKeyAuth))
|
|
|
apiFile.Use(middleware.BasicAuth(apiBasicAuth))
|
|
|
/////////////////////////////////////////////
|
|
|
|
|
|
/////////////////////////////////////////////
|
|
|
// ROUTES:
|
|
|
|
|
|
e.GET("/", nothingResponse)
|
|
|
e.HEAD("/", uptimeCheck)
|
|
|
e.HEAD("/up", uptimeCheck)
|
|
|
e.GET("/up", uptimeCheck)
|
|
|
e.HEAD("/_up", uptimeCheck)
|
|
|
e.GET("/_up", uptimeCheck)
|
|
|
|
|
|
e.Static("/.well-known/acme-challenge", leapiconf.LetsEncryptValidationPath) //Lets Encrypt validation path
|
|
|
|
|
|
/////////////////////////////////
|
|
|
// API Routes //
|
|
|
/////////////////////////////////
|
|
|
|
|
|
api.OPTIONS("/domains", apiListDomains)
|
|
|
api.GET("/domains", apiListDomains)
|
|
|
api.OPTIONS("/domains/:domain", apiPutDomain)
|
|
|
api.PUT("/domains/:domain", apiPutDomain)
|
|
|
api.DELETE("/domains/:domain", apiDeleteDomain)
|
|
|
|
|
|
api.OPTIONS("/servers", apiListServers)
|
|
|
api.GET("/servers", apiListServers)
|
|
|
api.OPTIONS("/servers/:server", apiPutServer)
|
|
|
api.PUT("/servers/:server", apiPutServer)
|
|
|
api.DELETE("/servers/:server", apiDeleteServer)
|
|
|
|
|
|
api.OPTIONS("/sync/:host", apiSync)
|
|
|
api.POST("/sync/:host", apiSync)
|
|
|
|
|
|
api.OPTIONS("/renew", apiRenew)
|
|
|
api.POST("/renew", apiRenew)
|
|
|
|
|
|
api.OPTIONS("/reload", apiReload)
|
|
|
api.POST("/reload", apiReload)
|
|
|
|
|
|
apiFile.OPTIONS("/upload/:fileType", apiUpload)
|
|
|
apiFile.PUT("/upload/:fileType", apiUpload)
|
|
|
|
|
|
apiFile.OPTIONS("/sync/:fileType", apiUploadSync)
|
|
|
apiFile.PUT("/sync/:fileType", apiUploadSync)
|
|
|
|
|
|
/////////////////////////////////////////////
|
|
|
// HTTP SERVERS CONFIG:
|
|
|
|
|
|
//TLS Server
|
|
|
if leapiconf.HTTPS_ServerPort != "-" { //disable HTTPS if port is zero
|
|
|
|
|
|
syncScheme = "https://"
|
|
|
syncPort = leapiconf.HTTPS_ServerPort
|
|
|
|
|
|
//certPair, err := tls.LoadX509KeyPair(leapiconf.TLSCertificateFile, leapiconf.TLSKeyFile)
|
|
|
if !fileExists(leapiconf.TLSChainFile) || !fileExists(leapiconf.TLSKeyFile) {
|
|
|
fmt.Println("Provided certificate and/or key file does not exist! Terminating.")
|
|
|
log.Fatal("Provided certificate and/or key file does not exist! Terminating.")
|
|
|
}
|
|
|
|
|
|
//Create loader for cert files
|
|
|
kpr, err := NewKeypairReloader(leapiconf.TLSChainFile, leapiconf.TLSKeyFile)
|
|
|
if err != nil {
|
|
|
log.Fatal(err)
|
|
|
}
|
|
|
|
|
|
tlsConfig := &tls.Config{
|
|
|
//MinVersion: tls.VersionTLS10,
|
|
|
MinVersion: tls.VersionTLS12,
|
|
|
//CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
|
|
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, //these two have fast assembly implementations
|
|
|
PreferServerCipherSuites: true,
|
|
|
//Certificates: []tls.Certificate{certPair},
|
|
|
//Use loader instead of certPair
|
|
|
GetCertificate: kpr.GetCertificateFunc(),
|
|
|
}
|
|
|
|
|
|
srvTLS := &http.Server{
|
|
|
Addr: ":" + leapiconf.HTTPS_ServerPort,
|
|
|
ReadTimeout: 180 * time.Second,
|
|
|
WriteTimeout: 180 * time.Second,
|
|
|
IdleTimeout: 180 * time.Second,
|
|
|
TLSConfig: tlsConfig,
|
|
|
}
|
|
|
|
|
|
//Start TLS Server
|
|
|
go func(c *echo.Echo) {
|
|
|
e.Logger.Fatal(e.StartServer(srvTLS))
|
|
|
}(e)
|
|
|
}
|
|
|
|
|
|
//HTTP Server
|
|
|
srvHTTP := &http.Server{
|
|
|
Addr: ":" + leapiconf.HTTP_ServerPort,
|
|
|
ReadTimeout: 180 * time.Second,
|
|
|
WriteTimeout: 180 * time.Second,
|
|
|
IdleTimeout: 180 * time.Second,
|
|
|
}
|
|
|
|
|
|
//Start HTTP Server
|
|
|
e.Logger.Fatal(e.StartServer(srvHTTP))
|
|
|
|
|
|
} //func main()
|
|
|
|
|
|
func (f *LEAPIConfig) checkConfig() {
|
|
|
var invalid bool
|
|
|
val := reflect.ValueOf(f).Elem()
|
|
|
fmt.Println()
|
|
|
for i := 0; i < val.NumField(); i++ {
|
|
|
valueField := val.Field(i)
|
|
|
if valueField.Interface() == "" || valueField.Interface() == nil || valueField.Interface() == 0 {
|
|
|
if !invalid {
|
|
|
log.Println("===========| ERRORS IN 'leapi_config.json' CONFIG FILE: |================")
|
|
|
color.Red("--------------------------------------------------------------------------------")
|
|
|
color.Red(" ===========| ERRORS IN 'leapi_config.json' CONFIG FILE: |================")
|
|
|
color.Red("--------------------------------------------------------------------------------")
|
|
|
}
|
|
|
invalid = true
|
|
|
log.Printf(" - Required config item '%s' missing or invalid.\n", val.Type().Field(i).Tag.Get("json"))
|
|
|
fmt.Printf(" - Required config item '%s' missing or invalid.\n", val.Type().Field(i).Tag.Get("json"))
|
|
|
}
|
|
|
}
|
|
|
if invalid {
|
|
|
color.Red("--------------------------------------------------------------------------------")
|
|
|
log.Fatal("Exiting!")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//This middleware adds headers to the response, for server version and CORS origin.
|
|
|
func serverHeaders(next echo.HandlerFunc) echo.HandlerFunc {
|
|
|
return func(c echo.Context) error {
|
|
|
c.Response().Header().Set(echo.HeaderServer, serverVersion)
|
|
|
c.Response().Header().Set("Access-Control-Allow-Origin", leapiconf.FrontEndURL)
|
|
|
c.Response().Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE")
|
|
|
//c.Response().Header().Set("Access-Control-Allow-Headers", leapiconf.AllowedHeaders)
|
|
|
c.Response().Header().Set("Access-Control-Allow-Headers",
|
|
|
strings.Join([]string{
|
|
|
echo.HeaderOrigin,
|
|
|
"X-Auth-Token",
|
|
|
echo.HeaderContentType,
|
|
|
echo.HeaderAccept,
|
|
|
echo.HeaderAuthorization,
|
|
|
}, ", "))
|
|
|
|
|
|
return next(c)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func apiKeyAuth(key string, c echo.Context) (bool, error) {
|
|
|
return (key == leapiconf.SecretKey), nil
|
|
|
}
|
|
|
|
|
|
func apiBasicAuth(username, password string, c echo.Context) (bool, error) {
|
|
|
return ((username == leapiconf.Username) && (password == leapiconf.SecretKey)), nil
|
|
|
}
|
|
|
|
|
|
func NewKeypairReloader(certPath, keyPath string) (*keypairReloader, error) {
|
|
|
result := &keypairReloader{
|
|
|
certPath: certPath,
|
|
|
keyPath: keyPath,
|
|
|
}
|
|
|
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
result.cert = &cert
|
|
|
go func() {
|
|
|
c := make(chan os.Signal, 1)
|
|
|
signal.Notify(c, syscall.SIGHUP)
|
|
|
for range c {
|
|
|
log.Printf("Received SIGHUP, reloading TLS certificate and key from %q and %q", leapiconf.TLSChainFile, leapiconf.TLSKeyFile)
|
|
|
fmt.Printf("Received SIGHUP, reloading TLS certificate and key from %q and %q\n", leapiconf.TLSChainFile, leapiconf.TLSKeyFile)
|
|
|
if err := result.maybeReload(); err != nil {
|
|
|
log.Printf("Keeping old TLS certificate because the new one could not be loaded: %v", err)
|
|
|
fmt.Printf("Keeping old TLS certificate because the new one could not be loaded: %v", err)
|
|
|
}
|
|
|
}
|
|
|
}()
|
|
|
return result, nil
|
|
|
}
|
|
|
|
|
|
func (kpr *keypairReloader) maybeReload() error {
|
|
|
newCert, err := tls.LoadX509KeyPair(kpr.certPath, kpr.keyPath)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
kpr.certMu.Lock()
|
|
|
defer kpr.certMu.Unlock()
|
|
|
kpr.cert = &newCert
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (kpr *keypairReloader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
|
return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
|
kpr.certMu.RLock()
|
|
|
defer kpr.certMu.RUnlock()
|
|
|
return kpr.cert, nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func okOut() (int, APIOutput) {
|
|
|
var out APIOutput
|
|
|
out.Message = "success"
|
|
|
out.Status = http.StatusOK
|
|
|
return out.Status, out
|
|
|
}
|
|
|
|
|
|
func errorOut(status int, msg string) (int, APIOutput) {
|
|
|
var out APIOutput
|
|
|
out.Message = msg
|
|
|
out.Status = status
|
|
|
return out.Status, out
|
|
|
}
|
|
|
|
|
|
func generateUUID() string {
|
|
|
return strings.Replace(uuid.New().String(), "-", "", -1)
|
|
|
}
|
|
|
|
|
|
func fileExists(filename string) bool {
|
|
|
info, err := os.Stat(filename)
|
|
|
if os.IsNotExist(err) {
|
|
|
return false
|
|
|
}
|
|
|
return !info.IsDir()
|
|
|
}
|
|
|
|
|
|
func writeDomains() error {
|
|
|
b := new(bytes.Buffer)
|
|
|
err := json.NewEncoder(b).Encode(domains)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't encode domains list into JSON: " + err.Error())
|
|
|
}
|
|
|
|
|
|
err = ioutil.WriteFile(configDir+"/domains.json", b.Bytes(), 0644)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't write domains file: " + configDir + "/domains.json")
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func writeServers() error {
|
|
|
b := new(bytes.Buffer)
|
|
|
err := json.NewEncoder(b).Encode(servers)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't encode servers list into JSON: " + err.Error())
|
|
|
}
|
|
|
|
|
|
err = ioutil.WriteFile(configDir+"/servers.json", b.Bytes(), 0644)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't write servers file: " + configDir + "/servers.json")
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func syncAllServers() error {
|
|
|
var theError error
|
|
|
numservers := len(servers)
|
|
|
c := make(chan string)
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
wg.Add(numservers)
|
|
|
for n := 0; n < numservers; n++ {
|
|
|
go func(c chan string) {
|
|
|
for {
|
|
|
srv, more := <-c
|
|
|
if more == false {
|
|
|
wg.Done()
|
|
|
return
|
|
|
}
|
|
|
|
|
|
log.Println("Parallel execution sync of server: " + srv + "...")
|
|
|
err := syncOneServer(srv)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
theError = err
|
|
|
}
|
|
|
}
|
|
|
}(c)
|
|
|
}
|
|
|
for _, server := range servers { //send each server to the channel
|
|
|
if server == leapiconf.Hostname { //don't send myself
|
|
|
continue
|
|
|
}
|
|
|
c <- server
|
|
|
}
|
|
|
close(c)
|
|
|
wg.Wait()
|
|
|
log.Println("Finished sending sync requests.")
|
|
|
|
|
|
return theError //if any one or more fail, return an error for it (the last one that fails)
|
|
|
}
|
|
|
|
|
|
func syncOneServer(server string) error {
|
|
|
//Make http requests to each other servers' /sync endpoints
|
|
|
// https://server.tld:port/sync
|
|
|
log.Println("SYNC " + server + " starting...")
|
|
|
req, err := http.NewRequest("POST", syncScheme+server+":"+syncPort+"/api/sync/"+leapiconf.Hostname, nil)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't create new HTTP sync request for server: " + server)
|
|
|
}
|
|
|
req.Close = true
|
|
|
req.Header.Set("User-Agent", myUserAgent)
|
|
|
req.Header.Set("Authorization", "Bearer "+leapiconf.SecretKey)
|
|
|
//skip verification of cert for https syncing, since the cert may not be setup properly at first
|
|
|
customTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
|
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
|
client := &http.Client{Transport: customTransport, Timeout: timeout}
|
|
|
//client := &http.Client{Timeout: timeout}
|
|
|
response, err := client.Do(req)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't perform HTTP sync request to server: " + server)
|
|
|
}
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't parse response body on request to server: " + server)
|
|
|
}
|
|
|
if response.StatusCode != 200 {
|
|
|
errorString := "Problem syncing to server " + server + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body)
|
|
|
log.Println(errorString)
|
|
|
return errors.New(errorString)
|
|
|
}
|
|
|
log.Println("SYNC " + server + " success!")
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func syncServersFromHost(host string) error {
|
|
|
var theError error
|
|
|
req, err := http.NewRequest("GET", syncScheme+host+":"+syncPort+"/api/servers", nil)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't create new HTTP request for syncing servers from host: " + host)
|
|
|
}
|
|
|
req.Close = true
|
|
|
req.Header.Set("User-Agent", myUserAgent)
|
|
|
req.Header.Set("Authorization", "Bearer "+leapiconf.SecretKey)
|
|
|
//skip verification of cert for https syncing, since the cert may not be setup properly at first
|
|
|
customTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
|
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
|
client := &http.Client{Transport: customTransport, Timeout: timeout}
|
|
|
//client := &http.Client{Timeout: timeout}
|
|
|
response, err := client.Do(req)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't perform HTTP server sync request to host: " + host)
|
|
|
}
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't parse response body from server sync request to server: " + host)
|
|
|
}
|
|
|
if response.StatusCode != 200 {
|
|
|
theError = errors.New("Problem syncing servers from host " + host + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body))
|
|
|
log.Println(theError.Error())
|
|
|
return theError
|
|
|
}
|
|
|
|
|
|
var result APIOutput
|
|
|
err = json.Unmarshal(body, &result)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't parse response body from host " + host + ": " + err.Error())
|
|
|
}
|
|
|
servers = result.Data
|
|
|
|
|
|
err = writeServers()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func syncDomainsFromHost(host string) error {
|
|
|
var theError error
|
|
|
req, err := http.NewRequest("GET", syncScheme+host+":"+syncPort+"/api/domains", nil)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't create new HTTP request for syncing domains from host: " + host)
|
|
|
}
|
|
|
req.Close = true
|
|
|
req.Header.Set("User-Agent", myUserAgent)
|
|
|
req.Header.Set("Authorization", "Bearer "+leapiconf.SecretKey)
|
|
|
//skip verification of cert for https syncing, since the cert may not be setup properly at first
|
|
|
customTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
|
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
|
client := &http.Client{Transport: customTransport, Timeout: timeout}
|
|
|
//client := &http.Client{Timeout: timeout}
|
|
|
response, err := client.Do(req)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't perform HTTP domain sync request to host: " + host)
|
|
|
}
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't parse response body from domain sync request to server: " + host)
|
|
|
}
|
|
|
if response.StatusCode != 200 {
|
|
|
theError = errors.New("Problem syncing domains from host " + host + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body))
|
|
|
log.Println(theError.Error())
|
|
|
return theError
|
|
|
}
|
|
|
|
|
|
var result APIOutput
|
|
|
err = json.Unmarshal(body, &result)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't parse response body from host " + host + ": " + err.Error())
|
|
|
}
|
|
|
domains = result.Data
|
|
|
|
|
|
err = writeDomains()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func sendFileToAllServers(filePath string) error {
|
|
|
var theError error
|
|
|
numservers := len(servers)
|
|
|
c := make(chan string)
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
wg.Add(numservers)
|
|
|
for n := 0; n < numservers; n++ {
|
|
|
go func(c chan string) {
|
|
|
for {
|
|
|
srv, more := <-c
|
|
|
if more == false {
|
|
|
wg.Done()
|
|
|
return
|
|
|
}
|
|
|
|
|
|
log.Println("Parallel execution send file to server: " + srv + "...")
|
|
|
err := sendFileToServer(filePath, srv)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
theError = err
|
|
|
}
|
|
|
}
|
|
|
}(c)
|
|
|
}
|
|
|
for _, server := range servers { //send each server to the channel
|
|
|
if server == leapiconf.Hostname { //don't send myself
|
|
|
continue
|
|
|
}
|
|
|
c <- server
|
|
|
}
|
|
|
close(c)
|
|
|
wg.Wait()
|
|
|
if theError == nil {
|
|
|
log.Println("Finished sending file " + filePath + " to all servers.")
|
|
|
}
|
|
|
|
|
|
err := os.Remove(filePath)
|
|
|
if err != nil {
|
|
|
log.Println("Error deleting temporary file: " + filePath + " - " + err.Error())
|
|
|
}
|
|
|
return theError //if any one or more fail, return an error for it (the last one that fails)
|
|
|
}
|
|
|
|
|
|
func sendFileToServer(filePath, server string) error {
|
|
|
log.Println("Send file " + filePath + " to " + server + " starting...")
|
|
|
|
|
|
_, fileName := path.Split(filePath)
|
|
|
dest := strings.SplitN(fileName, "__", 2)[0] //cert__abcdef1234567890.tmpfile --> "cert"
|
|
|
data, err := os.Open(filePath)
|
|
|
if err != nil {
|
|
|
return errors.New("sendFileToServer: Could not open temporary file " + filePath + ": " + err.Error())
|
|
|
}
|
|
|
url := syncScheme + server + ":" + syncPort + "/api/file/upload/" + dest
|
|
|
log.Println("Send file " + filePath + " to " + url + "...")
|
|
|
|
|
|
req, err := http.NewRequest("PUT", url, data)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't create new HTTP file upload request for server: " + server)
|
|
|
}
|
|
|
req.Close = true
|
|
|
req.Header.Set("User-Agent", myUserAgent)
|
|
|
req.SetBasicAuth(leapiconf.Username, leapiconf.SecretKey)
|
|
|
//req.Header.Set("Authorization", "Bearer "+leapiconf.SecretKey)
|
|
|
//skip verification of cert, since the cert may not be setup properly at first
|
|
|
customTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
|
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
|
client := &http.Client{Transport: customTransport, Timeout: timeout}
|
|
|
//client := &http.Client{Timeout: timeout}
|
|
|
response, err := client.Do(req)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't do HTTP file upload to server: " + server)
|
|
|
}
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return errors.New("Couldn't parse response body on request to server: " + server)
|
|
|
}
|
|
|
if response.StatusCode != 200 {
|
|
|
errorString := "Problem uploading file to server " + server + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body)
|
|
|
log.Println(errorString)
|
|
|
return errors.New(errorString)
|
|
|
}
|
|
|
log.Println("Upload [" + dest + "] to " + server + " success!")
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func renew() error {
|
|
|
log.Println("Renew operation initiated...")
|
|
|
//BUILD/SET GETSSL ENVIRONMENT VARIABLES THEN EXECUTE GETSSL
|
|
|
|
|
|
//domain list
|
|
|
var domainlist string
|
|
|
for _, d := range domains {
|
|
|
if d == leapiconf.PrimaryDomain { //ignore primary domain
|
|
|
continue
|
|
|
}
|
|
|
domainlist = domainlist + "," + d
|
|
|
}
|
|
|
domainlist = strings.TrimLeft(domainlist, ",") //Take off leading comma
|
|
|
err := os.Setenv("SANS", domainlist)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting SANS domains list environment variable: " + err.Error())
|
|
|
}
|
|
|
if leapiconf.Debug {
|
|
|
log.Println(domainlist)
|
|
|
}
|
|
|
|
|
|
//ACL string
|
|
|
aclstring := leapiconf.LetsEncryptValidationPath
|
|
|
if leapiconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == leapiconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
aclstring += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.LetsEncryptValidationPath
|
|
|
//aclstring += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
aclstring += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.HTTPS_ServerPort + ":/api/file/sync"
|
|
|
//aclstring += " ;cmd:\"curl -s -k -u" + leapiconf.Username + "\\:" + leapiconf.SecretKey + " " + syncScheme + leapiconf.Hostname + ":" + syncPort + "/api/file/upload/" + "$destfile" + " -T $src \"" + ":" + leapiconf.LetsEncryptValidationPath
|
|
|
}
|
|
|
err = os.Setenv("ACL", aclstring)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting ACL environment variable: " + err.Error())
|
|
|
}
|
|
|
|
|
|
if leapiconf.Debug {
|
|
|
log.Println("ACL STRING:")
|
|
|
log.Println(aclstring)
|
|
|
}
|
|
|
|
|
|
//Cert and key locations
|
|
|
domain_cert_location := leapiconf.TLSCertFile
|
|
|
if leapiconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == leapiconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
domain_cert_location += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.TLSCertFile
|
|
|
//domain_cert_location += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/cert"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
domain_cert_location += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.HTTPS_ServerPort + ":/api/file/sync/cert"
|
|
|
}
|
|
|
err = os.Setenv("DOMAIN_CERT_LOCATION", domain_cert_location)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting DOMAIN_CERT_LOCATION environment variable: " + err.Error())
|
|
|
}
|
|
|
|
|
|
domain_key_location := leapiconf.TLSKeyFile
|
|
|
if leapiconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == leapiconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
domain_key_location += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.TLSKeyFile
|
|
|
//domain_key_location += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/key"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
domain_key_location += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.HTTPS_ServerPort + ":/api/file/sync/key"
|
|
|
}
|
|
|
err = os.Setenv("DOMAIN_KEY_LOCATION", domain_key_location)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting DOMAIN_KEY_LOCATION environment variable: " + err.Error())
|
|
|
}
|
|
|
|
|
|
domain_chain_location := leapiconf.TLSChainFile
|
|
|
if leapiconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == leapiconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
//domain_chain_location += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.TLSChainFile
|
|
|
domain_chain_location += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/chain"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
domain_chain_location += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.HTTPS_ServerPort + ":/api/file/sync/chain"
|
|
|
}
|
|
|
err = os.Setenv("DOMAIN_CHAIN_LOCATION", domain_chain_location)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting DOMAIN_CHAIN_LOCATION environment variable: " + err.Error())
|
|
|
}
|
|
|
|
|
|
domain_pem_location := leapiconf.TLSPEMFile
|
|
|
if leapiconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == leapiconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
domain_pem_location += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.TLSPEMFile
|
|
|
//domain_pem_location += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/pem"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
domain_pem_location += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.HTTPS_ServerPort + ":/api/file/sync/pem"
|
|
|
}
|
|
|
err = os.Setenv("DOMAIN_PEM_LOCATION", domain_pem_location)
|
|
|
if err != nil {
|
|
|
return errors.New("RENEW: error setting DOMAIN_PEM_LOCATION environment variable: " + err.Error())
|
|
|
}
|
|
|
|
|
|
//these parameters don't seem to be respected by gettssl from environment variables, so write them to config file:
|
|
|
ca_cert_location := leapiconf.TLSCAFile
|
|
|
if leapiconf.SyncType == "ssh" {
|
|
|
for _, server := range servers {
|
|
|
if server == leapiconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
ca_cert_location += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.TLSCAFile
|
|
|
//ca_cert_location += ";davs:leapi:" + leapiconf.SecretKey + ":" + server + ":" + syncPort + ":/api/file/upload/ca"
|
|
|
}
|
|
|
} else { //file sync type is HTTPS
|
|
|
ca_cert_location += ";davs:" + leapiconf.Username + ":" + leapiconf.SecretKey + ":" + leapiconf.Hostname + ":" + leapiconf.HTTPS_ServerPort + ":/api/file/sync/ca"
|
|
|
}
|
|
|
|
|
|
reload_command := leapiconf.ReloadCommand
|
|
|
for _, server := range servers {
|
|
|
if server == leapiconf.Hostname {
|
|
|
continue
|
|
|
}
|
|
|
//old ssh method; requires ssh key
|
|
|
//reload_command += "; ssh " + leapiconf.Username + "@" + server + " '" + leapiconf.ReloadCommand + "'"
|
|
|
//new method; calls LEAPI to trigger reload
|
|
|
reload_command += " ; curl -s -k -X POST -H 'Authorization: Bearer " + leapiconf.SecretKey + "' " + syncScheme + server + "/api/reload"
|
|
|
//reload_command += "; 'curl -s -X POST -H \\\"Authorization: Bearer " + leapiconf.SecretKey + "\\\" " + syncScheme + server + "/api/reload"
|
|
|
}
|
|
|
|
|
|
ca_server := "https://acme-staging-v02.api.letsencrypt.org"
|
|
|
if leapiconf.Production {
|
|
|
ca_server = "https://acme-v02.api.letsencrypt.org"
|
|
|
}
|
|
|
|
|
|
var configFile string
|
|
|
|
|
|
configFile = "CA=\"" + ca_server + "\"\n"
|
|
|
configFile += "USE_SINGLE_ACL=\"true\"\n"
|
|
|
configFile += "CA_CERT_LOCATION=\"" + leapiconf.TLSCAFile + "\"\n"
|
|
|
configFile += "RELOAD_CMD=\"" + reload_command + "\"\n"
|
|
|
configFile += "RENEW_ALLOW=\"" + leapiconf.RenewAllow + "\"\n"
|
|
|
configFile += "CHECK_REMOTE=\"true\"\n"
|
|
|
configFile += "SERVER_TYPE=\"" + leapiconf.CheckPort + "\"\n"
|
|
|
configFile += "CHECK_REMOTE_WAIT=\"5\"\n"
|
|
|
|
|
|
//write config file
|
|
|
err = ioutil.WriteFile(configDir+"/"+leapiconf.PrimaryDomain+"/getssl.cfg", []byte(configFile), 0644)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't write getssl config file: " + configDir + "/" + leapiconf.PrimaryDomain + "/getssl.cfg")
|
|
|
}
|
|
|
|
|
|
if leapiconf.Debug {
|
|
|
//////PRINT VARS
|
|
|
for _, e := range os.Environ() {
|
|
|
log.Println(e)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//RUN GETSSL
|
|
|
//first patch getssl to disable cert verification checking:
|
|
|
cmd := exec.Command("/usr/bin/sed", "-i", "s/NOMETER} -u/NOMETER} -k -u/g", leapiconf.SrvDir+"/getssl")
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
if err != nil {
|
|
|
log.Println(string(output))
|
|
|
return errors.New("RENEW: patching of getssl to disable curl certificate verification during LEAPI sync failed: " + err.Error())
|
|
|
}
|
|
|
//RUN getssl on primary domain to renew
|
|
|
//cmd = exec.Command(leapiconf.SrvDir+"/getssl", "-u", "-w", leapiconf.SrvDir, leapiconf.PrimaryDomain)
|
|
|
if leapiconf.Debug {
|
|
|
cmd = exec.Command(leapiconf.SrvDir+"/getssl", "-d", "-w", leapiconf.SrvDir, leapiconf.PrimaryDomain)
|
|
|
} else {
|
|
|
cmd = exec.Command(leapiconf.SrvDir+"/getssl", "-w", leapiconf.SrvDir, leapiconf.PrimaryDomain)
|
|
|
}
|
|
|
output, err = cmd.CombinedOutput()
|
|
|
if err != nil {
|
|
|
log.Println("BEGIN GETSSL OUTPUT:")
|
|
|
log.Println(string(output))
|
|
|
log.Println("END GETSSL OUTPUT")
|
|
|
return errors.New("RENEW: execution of getssl failed: " + err.Error() + " Check log file " + leapiconf.LogFile + " for more details.")
|
|
|
}
|
|
|
log.Println("BEGIN GETSSL OUTPUT:")
|
|
|
log.Println(string(output))
|
|
|
log.Println("END GETSSL OUTPUT")
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func reload() error {
|
|
|
//To avoid problems with spaces in the command, we build a script file and run it in shell, rather than directly.
|
|
|
reloadScript := "#!/bin/sh\n"
|
|
|
reloadScript += leapiconf.ReloadCommand + "\n"
|
|
|
|
|
|
//write script file to run reload command[s]
|
|
|
err := ioutil.WriteFile(configDir+"/reloadscript.sh", []byte(reloadScript), 0755)
|
|
|
if err != nil {
|
|
|
return errors.New("Couldn't write reload script file: " + configDir + "/reloadscript.sh")
|
|
|
}
|
|
|
|
|
|
cmd := exec.Command(leapiconf.SrvDir + "/reloadscript.sh")
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
if err != nil {
|
|
|
log.Println("BEGIN RELOADSCRIPT OUTPUT:")
|
|
|
log.Println(string(output))
|
|
|
log.Println("END RELOADSCRIPT OUTPUT")
|
|
|
return errors.New("RELOAD: execution of reload script failed: " + err.Error())
|
|
|
}
|
|
|
log.Println("BEGIN RELOADSCRIPT OUTPUT:")
|
|
|
log.Println(string(output))
|
|
|
log.Println("END RELOADSCRIPT OUTPUT")
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func uptime() UpOut {
|
|
|
uptime := fmt.Sprintf("%s", time.Since(startupTime))
|
|
|
|
|
|
out := UpOut{
|
|
|
Up: true,
|
|
|
StartTime: startupTime,
|
|
|
Uptime: uptime,
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////
|
|
|
///// API ROUTE FUNCTIONS
|
|
|
/////////////////////////////////////////////
|
|
|
func nothingResponse(c echo.Context) error {
|
|
|
return c.NoContent(http.StatusNotFound)
|
|
|
}
|
|
|
|
|
|
func uptimeCheck(c echo.Context) error {
|
|
|
if c.Request().Method == http.MethodHead {
|
|
|
return c.NoContent(http.StatusOK)
|
|
|
}
|
|
|
//return c.String(http.StatusOK, "{\"up\":true}")
|
|
|
return c.JSON(http.StatusOK, uptime())
|
|
|
}
|
|
|
|
|
|
func apiRenew(c echo.Context) error {
|
|
|
err := renew()
|
|
|
if err != nil {
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
|
|
|
}
|
|
|
return c.JSON(okOut())
|
|
|
}
|
|
|
|
|
|
func apiReload(c echo.Context) error {
|
|
|
err := reload()
|
|
|
if err != nil {
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error reloading services: "+err.Error()))
|
|
|
}
|
|
|
return c.JSON(okOut())
|
|
|
}
|
|
|
|
|
|
func apiUpload(c echo.Context) error {
|
|
|
fileType := c.Param("fileType")
|
|
|
r := c.Request()
|
|
|
|
|
|
var filePath string
|
|
|
switch fileType {
|
|
|
case "ca":
|
|
|
filePath = leapiconf.TLSCAFile
|
|
|
case "chain":
|
|
|
filePath = leapiconf.TLSChainFile
|
|
|
case "key":
|
|
|
filePath = leapiconf.TLSKeyFile
|
|
|
case "cert":
|
|
|
filePath = leapiconf.TLSCertFile
|
|
|
case "pem":
|
|
|
filePath = leapiconf.TLSPEMFile
|
|
|
default: //ACL
|
|
|
//return c.JSON(errorOut(http.StatusBadRequest, "Invalid filetype/URL."))
|
|
|
filePath = leapiconf.LetsEncryptValidationPath + "/" + fileType
|
|
|
}
|
|
|
|
|
|
directory, _ := path.Split(filePath)
|
|
|
|
|
|
//Check and create directory
|
|
|
if _, err := os.Stat(directory); os.IsNotExist(err) {
|
|
|
err = os.MkdirAll(directory, 0755)
|
|
|
if err != nil {
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Filetype "+fileType+" directory does not exist, and could not create: "+err.Error()))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//Read the upload data
|
|
|
var blimit int64 = 102400 //100k max upload size
|
|
|
body, err := ioutil.ReadAll(io.LimitReader(r.Body, blimit))
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error reading post body: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
//Write the file
|
|
|
err = ioutil.WriteFile(filePath, body, 0644)
|
|
|
if err != nil {
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Could not write file: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
log.Println("Received PUT to " + r.RequestURI)
|
|
|
log.Println("Writing to " + filePath)
|
|
|
|
|
|
return c.JSON(okOut())
|
|
|
}
|
|
|
|
|
|
func apiUploadSync(c echo.Context) error {
|
|
|
fileType := c.Param("fileType")
|
|
|
r := c.Request()
|
|
|
|
|
|
//Read the upload data
|
|
|
var blimit int64 = 102400 //100k max upload size
|
|
|
body, err := ioutil.ReadAll(io.LimitReader(r.Body, blimit))
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error reading post body: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
uuid := generateUUID()
|
|
|
filePath := leapiconf.SrvDir + "/" + fileType + "__" + uuid + ".tmpfile"
|
|
|
|
|
|
//Write the file
|
|
|
err = ioutil.WriteFile(filePath, body, 0644)
|
|
|
if err != nil {
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Could not write temporary file: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
log.Println("Received PUT for sync to " + r.RequestURI)
|
|
|
log.Println("Writing to " + filePath)
|
|
|
|
|
|
err = sendFileToAllServers(filePath)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error sending file "+filePath+" to other servers: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
return c.JSON(okOut())
|
|
|
}
|
|
|
|
|
|
func apiListDomains(c echo.Context) error {
|
|
|
var out APIOutput
|
|
|
out.Status = http.StatusOK
|
|
|
out.Message = "domains list"
|
|
|
out.Data = domains
|
|
|
return c.JSON(out.Status, out)
|
|
|
}
|
|
|
|
|
|
func apiPutDomain(c echo.Context) error {
|
|
|
domain := c.Param("domain")
|
|
|
|
|
|
//check for dups
|
|
|
for _, d := range domains {
|
|
|
if d == domain {
|
|
|
return c.JSON(errorOut(http.StatusBadRequest, "Bad request: Domain already exists."))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//add domain to list
|
|
|
domains = append(domains, domain)
|
|
|
|
|
|
//write list to disk
|
|
|
err := writeDomains()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing domains list to disk: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
//sync with other servers
|
|
|
err = syncAllServers()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
//renew cert
|
|
|
err = renew()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
return c.JSON(okOut())
|
|
|
}
|
|
|
|
|
|
func apiDeleteDomain(c echo.Context) error {
|
|
|
deleteDomain := c.Param("domain")
|
|
|
var newlist []string
|
|
|
for _, d := range domains {
|
|
|
if d != deleteDomain {
|
|
|
newlist = append(newlist, d)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
domains = newlist
|
|
|
|
|
|
//write list to disk
|
|
|
err := writeDomains()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing domains list to disk: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
//sync with other servers
|
|
|
err = syncAllServers()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
//renew cert
|
|
|
err = renew()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
return c.JSON(okOut())
|
|
|
}
|
|
|
|
|
|
func apiListServers(c echo.Context) error {
|
|
|
var out APIOutput
|
|
|
out.Status = http.StatusOK
|
|
|
out.Message = "servers list"
|
|
|
out.Data = servers
|
|
|
return c.JSON(out.Status, out)
|
|
|
}
|
|
|
|
|
|
func apiPutServer(c echo.Context) error {
|
|
|
server := c.Param("server")
|
|
|
|
|
|
//check for dups
|
|
|
for _, s := range servers {
|
|
|
if s == server {
|
|
|
return c.JSON(errorOut(http.StatusBadRequest, "Bad request: Server already exists."))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//add servers to list
|
|
|
servers = append(servers, server)
|
|
|
|
|
|
//write list to disk
|
|
|
err := writeServers()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing servers list to disk: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
//sync with other servers
|
|
|
err = syncAllServers()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
//renew cert
|
|
|
err = renew()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
return c.JSON(okOut())
|
|
|
}
|
|
|
|
|
|
func apiDeleteServer(c echo.Context) error {
|
|
|
deleteServer := c.Param("server")
|
|
|
|
|
|
var newlist []string
|
|
|
for _, s := range servers {
|
|
|
if s != deleteServer {
|
|
|
newlist = append(newlist, s)
|
|
|
}
|
|
|
}
|
|
|
servers = newlist
|
|
|
|
|
|
//write list to disk
|
|
|
err := writeServers()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error writing servers list to disk: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
//sync with other servers
|
|
|
err = syncAllServers()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing to other servers: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
//renew cert
|
|
|
err = renew()
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error renewing: "+err.Error()))
|
|
|
}
|
|
|
|
|
|
return c.JSON(okOut())
|
|
|
}
|
|
|
|
|
|
func apiSync(c echo.Context) error {
|
|
|
host := c.Param("host")
|
|
|
|
|
|
log.Println("Received sync request for host: " + host + ". From IP address: " + c.RealIP() + " Syncing...")
|
|
|
|
|
|
err := syncServersFromHost(host)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing servers from host: "+host+". "+err.Error()))
|
|
|
}
|
|
|
|
|
|
err = syncDomainsFromHost(host)
|
|
|
if err != nil {
|
|
|
log.Println(err.Error())
|
|
|
return c.JSON(errorOut(http.StatusInternalServerError, "Error syncing domains from host: "+host+". "+err.Error()))
|
|
|
}
|
|
|
|
|
|
return c.JSON(okOut())
|
|
|
}
|