From 251beb24d30ee8a12129f26a00086692b5af4f78 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Thu, 21 Feb 2019 17:33:46 +0300
Subject: [PATCH] Added openapi description

---
 app.go               |   2 +-
 config.go            |  33 +++++-----
 control.go           |  24 +------
 helpers.go           |  29 ---------
 openapi/openapi.yaml | 148 ++++++++++++++++++++++++++++++++++++++++++-
 5 files changed, 165 insertions(+), 71 deletions(-)

diff --git a/app.go b/app.go
index 5485b94a..518bb397 100644
--- a/app.go
+++ b/app.go
@@ -173,7 +173,7 @@ func run(args options) {
 		for { // this is an endless loop
 			httpsServer.cond.L.Lock()
 			// this mechanism doesn't let us through until all conditions are ment
-			for config.TLS.Enabled == false || config.TLS.PortHTTPS == 0 || config.TLS.PrivateKey == "" || config.TLS.CertificateChain == "" { // sleep until neccessary data is supplied
+			for config.TLS.Enabled == false || config.TLS.PortHTTPS == 0 || config.TLS.PrivateKey == "" || config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
 				httpsServer.cond.Wait()
 			}
 			address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
diff --git a/config.go b/config.go
index c1aa2f8d..158381c1 100644
--- a/config.go
+++ b/config.go
@@ -63,37 +63,34 @@ type dnsConfig struct {
 var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
 
 type tlsConfigSettings struct {
-	Enabled        bool   `yaml:"enabled" json:"enabled"`
-	ServerName     string `yaml:"server_name" json:"server_name,omitempty"`
-	ForceHTTPS     bool   `yaml:"force_https" json:"force_https,omitempty"`
-	PortHTTPS      int    `yaml:"port_https" json:"port_https,omitempty"`
-	PortDNSOverTLS int    `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"`
+	Enabled        bool   `yaml:"enabled" json:"enabled"`                               // Enabled is the encryption (DOT/DOH/HTTPS) status
+	ServerName     string `yaml:"server_name" json:"server_name,omitempty"`             // ServerName is the hostname of your HTTPS/TLS server
+	ForceHTTPS     bool   `yaml:"force_https" json:"force_https,omitempty"`             // ForceHTTPS: if true, forces HTTP->HTTPS redirect
+	PortHTTPS      int    `yaml:"port_https" json:"port_https,omitempty"`               // HTTPS port. If 0, HTTPS will be disabled
+	PortDNSOverTLS int    `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"` // DNS-over-TLS port. If 0, DOT will be disabled
 
 	dnsforward.TLSConfig `yaml:",inline" json:",inline"`
 }
 
 // field ordering is not important -- these are for API and are recalculated on each run
 type tlsConfigStatus struct {
-	// certificate status
-	ValidCert         bool      `yaml:"-" json:"valid_cert"`
-	ValidChain        bool      `yaml:"-" json:"valid_chain"`
-	Subject           string    `yaml:"-" json:"subject,omitempty"`
-	Issuer            string    `yaml:"-" json:"issuer,omitempty"`
-	NotBefore         time.Time `yaml:"-" json:"not_before,omitempty"`
-	NotAfter          time.Time `yaml:"-" json:"not_after,omitempty"`
-	DNSNames          []string  `yaml:"-" json:"dns_names"`
-	StatusCertificate string    `yaml:"-" json:"status_cert,omitempty"`
+	ValidCert  bool      `yaml:"-" json:"valid_cert"`           // ValidCert is true if the specified certificates chain is a valid chain of X509 certificates
+	ValidChain bool      `yaml:"-" json:"valid_chain"`          // ValidChain is true if the specified certificates chain is verified and issued by a known CA
+	Subject    string    `yaml:"-" json:"subject,omitempty"`    // Subject is the subject of the first certificate in the chain
+	Issuer     string    `yaml:"-" json:"issuer,omitempty"`     // Issuer is the issuer of the first certificate in the chain
+	NotBefore  time.Time `yaml:"-" json:"not_before,omitempty"` // NotBefore is the NotBefore field of the first certificate in the chain
+	NotAfter   time.Time `yaml:"-" json:"not_after,omitempty"`  // NotAfter is the NotAfter field of the first certificate in the chain
+	DNSNames   []string  `yaml:"-" json:"dns_names"`            // DNSNames is the value of SubjectAltNames field of the first certificate in the chain
 
 	// key status
-	ValidKey bool   `yaml:"-" json:"valid_key"`
-	KeyType  string `yaml:"-" json:"key_type,omitempty"`
+	ValidKey bool   `yaml:"-" json:"valid_key"`          // ValidKey is true if the key is a valid private key
+	KeyType  string `yaml:"-" json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
 
 	// is usable? set by validator
 	usable bool
 
 	// warnings
-	Warning           string `yaml:"-" json:"warning,omitempty"`
-	WarningValidation string `yaml:"-" json:"warning_validation,omitempty"`
+	WarningValidation string `yaml:"-" json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
 }
 
 // field ordering is important -- yaml fields will mirror ordering from here
diff --git a/control.go b/control.go
index 4d13a9b8..a87bd2cd 100644
--- a/control.go
+++ b/control.go
@@ -1118,7 +1118,7 @@ func validateCertificates(data tlsConfig) tlsConfig {
 	// clear out status for certificates
 	data.tlsConfigStatus = tlsConfigStatus{}
 
-	// check only public certificate separetely from the key
+	// check only public certificate separately from the key
 	if data.CertificateChain != "" {
 		log.Tracef("got certificate: %s", data.CertificateChain)
 
@@ -1194,24 +1194,6 @@ func validateCertificates(data tlsConfig) tlsConfig {
 			data.NotAfter = notAfter
 			data.NotBefore = mainCert.NotBefore
 			data.DNSNames = mainCert.DNSNames
-
-			data.StatusCertificate = fmt.Sprintf("Certificate expires on %s", notAfter) //, valid for hostname %s", mainCert.NotAfter, mainCert.Subject.CommonName)
-			if len(mainCert.DNSNames) == 1 {
-				data.StatusCertificate += fmt.Sprintf(", valid for hostname %s", mainCert.DNSNames[0])
-			} else if len(mainCert.DNSNames) > 1 {
-				data.StatusCertificate += ", valid for hostnames " + strings.Join(mainCert.DNSNames, ", ")
-			}
-
-			// issue a warning if certificate is about to expire
-			now := time.Now()
-			if now.AddDate(0, 0, 30).After(notAfter) {
-				timeLeft := notAfter.Sub(now)
-				if timeLeft > 0 {
-					data.Warning = fmt.Sprintf("Your certificate expires in %.0f days, we recommend you update it soon", timeLeft.Hours()/24)
-				} else {
-					data.Warning = fmt.Sprintf("Your certificate has expired on %s, we recommend you update it immediatedly", mainCert.NotAfter)
-				}
-			}
 		}
 	}
 
@@ -1322,11 +1304,11 @@ func marshalTLS(w http.ResponseWriter, data tlsConfig) {
 	w.Header().Set("Content-Type", "application/json")
 	if data.CertificateChain != "" {
 		encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain))
-		data.CertificateChain = string(encoded)
+		data.CertificateChain = encoded
 	}
 	if data.PrivateKey != "" {
 		encoded := base64.StdEncoding.EncodeToString([]byte(data.PrivateKey))
-		data.PrivateKey = string(encoded)
+		data.PrivateKey = encoded
 	}
 	err := json.NewEncoder(w).Encode(data)
 	if err != nil {
diff --git a/helpers.go b/helpers.go
index cf2d3598..822c77df 100644
--- a/helpers.go
+++ b/helpers.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
-	"math/rand"
 	"net"
 	"net/http"
 	"net/url"
@@ -257,34 +256,6 @@ func checkPacketPortAvailable(host string, port int) error {
 	return err
 }
 
-// ------------------------
-// random string generation
-// ------------------------
-const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
-const (
-	letterIdxBits = 6                    // 6 bits to represent a letter index
-	letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
-	letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
-)
-
-func RandStringBytesMaskImpr(n int) string {
-	b := make([]byte, n)
-	// A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
-	for i, cache, remain := n-1, rand.Int63(), letterIdxMax; i >= 0; {
-		if remain == 0 {
-			cache, remain = rand.Int63(), letterIdxMax
-		}
-		if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
-			b[i] = letterBytes[idx]
-			i--
-		}
-		cache >>= letterIdxBits
-		remain--
-	}
-
-	return string(b)
-}
-
 // ---------------------
 // debug logging helpers
 // ---------------------
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index c9801b4b..452e5036 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -2,7 +2,7 @@ swagger: '2.0'
 info:
     title: 'AdGuard Home'
     description: 'AdGuard Home REST API. Admin web interface is built on top of this REST API.'
-    version: 0.92.0
+    version: 0.93.0
 schemes:
     - http
 basePath: /control
@@ -12,6 +12,9 @@ tags:
     -
         name: global
         description: 'AdGuard Home server general settings and controls'
+    -
+        name: tls
+        description: 'AdGuard Home HTTPS/DOH/DOT settings'
     -
         name: log
         description: 'AdGuard Home query log'
@@ -267,6 +270,70 @@ paths:
                 200:
                     description: OK
 
+    # --------------------------------------------------
+    # TLS server methods
+    # --------------------------------------------------
+
+    /tls/status:
+        get:
+            tags:
+                - tls
+            operationId: tlsStatus
+            summary: "Returns TLS configuration and its status"
+            responses:
+                200:
+                    description: OK
+                    schema:
+                        $ref: "#/definitions/TlsConfig"
+
+    /tls/configure:
+        post:
+            tags:
+                - tls
+            operationId: tlsConfigure
+            summary: "Updates current TLS configuration"
+            consumes:
+                - application/json
+            parameters:
+                - in: "body"
+                  name: "body"
+                  description: "TLS configuration JSON"
+                  required: true
+                  schema:
+                      $ref: "#/definitions/TlsConfig"
+            responses:
+                200:
+                    description: "TLS configuration and its status"
+                    schema:
+                        $ref: "#/definitions/TlsConfig"
+                400:
+                    description: "Invalid configuration or unavailable port"
+                500:
+                    description: "Error occurred while applying configuration"
+
+    /tls/validate:
+        post:
+            tags:
+                - tls
+            operationId: tlsValidate
+            summary: "Checks if the current TLS configuration is valid"
+            consumes:
+                - application/json
+            parameters:
+                - in: "body"
+                  name: "body"
+                  description: "TLS configuration JSON"
+                  required: true
+                  schema:
+                      $ref: "#/definitions/TlsConfig"
+            responses:
+                200:
+                    description: "TLS configuration and its status"
+                    schema:
+                        $ref: "#/definitions/TlsConfig"
+                400:
+                    description: "Invalid configuration or unavailable port"
+
     # --------------------------------------------------
     # DHCP server methods
     # --------------------------------------------------
@@ -1063,4 +1130,81 @@ definitions:
         type: "array"
         description: "Query log"
         items:
-            $ref: "#/definitions/QueryLogItem"
\ No newline at end of file
+            $ref: "#/definitions/QueryLogItem"
+    TlsConfig:
+        type: "object"
+        description: "TLS configuration settings and status"
+        properties:
+            # TLS configuration
+            enabled:
+                type: "boolean"
+                example: "true"
+                description: "enabled is the encryption (DOT/DOH/HTTPS) status"
+            server_name:
+                type: "string"
+                example: "example.org"
+                description: "server_name is the hostname of your HTTPS/TLS server"
+            force_https:
+                type: "boolean"
+                example: "true"
+                description: "if true, forces HTTP->HTTPS redirect"
+            port_https:
+                type: "integer"
+                format: "int32"
+                example: 443
+                description: "HTTPS port. If 0, HTTPS will be disabled."
+            port_dns_over_tls:
+                type: "integer"
+                format: "int32"
+                example: 853
+                description: "DNS-over-TLS port. If 0, DOT will be disabled."
+            certificate_chain:
+                type: "string"
+                description: "Base64 string with PEM-encoded certificates chain"
+            private_key:
+                type: "string"
+                description: "Base64 string with PEM-encoded private key"
+            # Below goes validation fields
+            valid_cert:
+                type: "boolean"
+                example: "true"
+                description: "valid_cert is true if the specified certificates chain is a valid chain of X509 certificates"
+            valid_chain:
+                type: "boolean"
+                example: "true"
+                description: "valid_chain is true if the specified certificates chain is verified and issued by a known CA"
+            subject:
+                type: "string"
+                example: "CN=example.org"
+                description: "subject is the subject of the first certificate in the chain"
+            issuer:
+                type: "string"
+                example: "CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US"
+                description: "issuer is the issuer of the first certificate in the chain"
+            not_before:
+                type: "string"
+                example: "2019-01-31T10:47:32Z"
+                description: "not_before is the NotBefore field of the first certificate in the chain"
+            not_after:
+                type: "string"
+                example: "2019-05-01T10:47:32Z"
+                description: "not_after is the NotAfter field of the first certificate in the chain"
+            dns_names:
+                type: "array"
+                items:
+                    type: "string"
+                description: "dns_names is the value of SubjectAltNames field of the first certificate in the chain"
+                example:
+                    - "*.example.org"
+            valid_key:
+                type: "boolean"
+                example: "true"
+                description: "valid_key is true if the key is a valid private key"
+            key_type:
+                type: "string"
+                example: "RSA"
+                description: "key_type is either RSA or ECDSA"
+            warning_validation:
+                type: "string"
+                example: "You have specified an empty certificate"
+                description: "warning_validation is a validation warning message with the issue description"
\ No newline at end of file