Browse Source

auto update script executed

Toby Chui 1 year ago
parent
commit
a194caf37b
8 changed files with 392 additions and 29 deletions
  1. 2 0
      api.go
  2. 89 7
      cert.go
  3. 51 14
      mod/dynamicproxy/dynamicproxy.go
  4. 24 2
      mod/tlscert/tlscert.go
  5. 14 1
      reverseproxy.go
  6. BIN
      sys.db
  7. 178 5
      web/components/cert.html
  8. 34 0
      web/components/status.html

+ 2 - 0
api.go

@@ -19,6 +19,8 @@ func initAPIs() {
 	http.HandleFunc("/setIncoming", HandleIncomingPortSet)
 
 	//TLS / SSL config
+	http.HandleFunc("/cert/tls", handleToggleTLSProxy)
 	http.HandleFunc("/cert/upload", handleCertUpload)
 	http.HandleFunc("/cert/list", handleListCertificate)
+	http.HandleFunc("/cert/checkDefault", handleDefaultCertCheck)
 }

+ 89 - 7
cert.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log"
 	"net/http"
 	"os"
 	"path/filepath"
@@ -11,6 +12,23 @@ import (
 	"imuslab.com/arozos/ReverseProxy/mod/utils"
 )
 
+//Check if the default certificates is correctly setup
+func handleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
+	type CheckResult struct {
+		DefaultPubExists bool
+		DefaultPriExists bool
+	}
+
+	pub, pri := tlsCertManager.DefaultCertExistsSep()
+	js, _ := json.Marshal(CheckResult{
+		pub,
+		pri,
+	})
+
+	sendJSONResponse(w, string(js))
+}
+
+//Return a list of domains where the certificates covers
 func handleListCertificate(w http.ResponseWriter, r *http.Request) {
 	filenames, err := tlsCertManager.ListCertDomains()
 	if err != nil {
@@ -18,14 +36,78 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	response, err := json.Marshal(filenames)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
+	showDate, _ := utils.GetPara(r, "date")
+	if showDate == "true" {
+		type CertInfo struct {
+			Domain           string
+			LastModifiedDate string
+		}
+
+		results := []*CertInfo{}
+
+		for _, filename := range filenames {
+			fileInfo, err := os.Stat(filepath.Join(tlsCertManager.CertStore, filename+".crt"))
+			if err != nil {
+				sendErrorResponse(w, "invalid domain certificate discovered: "+filename)
+				return
+			}
+			modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
+
+			thisCertInfo := CertInfo{
+				Domain:           filename,
+				LastModifiedDate: modifiedTime,
+			}
+
+			results = append(results, &thisCertInfo)
+		}
+
+		js, _ := json.Marshal(results)
+		w.Header().Set("Content-Type", "application/json")
+		w.Write(js)
+	} else {
+		response, err := json.Marshal(filenames)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		w.Header().Set("Content-Type", "application/json")
+		w.Write(response)
+	}
+
+}
+
+//Handle front-end toggling TLS mode
+func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
+	currentTlsSetting := false
+	if sysdb.KeyExists("settings", "usetls") {
+		sysdb.Read("settings", "usetls", &currentTlsSetting)
 	}
 
-	w.Header().Set("Content-Type", "application/json")
-	w.Write(response)
+	newState, err := utils.PostPara(r, "set")
+	if err != nil {
+		//No setting. Get the current status
+		js, _ := json.Marshal(currentTlsSetting)
+		utils.SendJSONResponse(w, string(js))
+	} else {
+		if newState == "true" {
+			sysdb.Write("settings", "usetls", true)
+			log.Println("Enabling TLS mode on reverse proxy")
+			dynamicProxyRouter.UpdateTLSSetting(true)
+			dynamicProxyRouter.Restart()
+		} else if newState == "false" {
+			sysdb.Write("settings", "usetls", false)
+			log.Println("Disabling TLS mode on reverse proxy")
+			dynamicProxyRouter.UpdateTLSSetting(false)
+			dynamicProxyRouter.Restart()
+		} else {
+			utils.SendErrorResponse(w, "invalid state given. Only support true or false")
+			return
+		}
+
+		sendOK(w)
+
+	}
 }
 
 //Handle upload of the certificate
@@ -48,7 +130,7 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
 	domain, err := utils.GetPara(r, "domain")
 	if err != nil {
 		//Assume localhost
-		domain = "localhost"
+		domain = "default"
 	}
 
 	if keytype == "pub" {

+ 51 - 14
mod/dynamicproxy/dynamicproxy.go

@@ -59,7 +59,7 @@ type ProxyHandler struct {
 	Parent *Router
 }
 
-func NewDynamicProxy(port int, tlsManager *tlscert.Manager) (*Router, error) {
+func NewDynamicProxy(port int, useTls bool, tlsManager *tlscert.Manager) (*Router, error) {
 	proxyMap := sync.Map{}
 	domainMap := sync.Map{}
 	thisRouter := Router{
@@ -68,7 +68,7 @@ func NewDynamicProxy(port int, tlsManager *tlscert.Manager) (*Router, error) {
 		SubdomainEndpoint: &domainMap,
 		Running:           false,
 		tlsCertManager:    tlsManager,
-		useTLS:            false,
+		useTLS:            useTls,
 		server:            nil,
 	}
 
@@ -79,6 +79,12 @@ func NewDynamicProxy(port int, tlsManager *tlscert.Manager) (*Router, error) {
 	return &thisRouter, nil
 }
 
+//Update TLS setting in runtime. Will restart the proxy server
+//if it is already running in the background
+func (router *Router) UpdateTLSSetting(tlsEnabled bool) {
+	router.useTLS = tlsEnabled
+}
+
 //Start the dynamic routing
 func (router *Router) StartProxyService() error {
 	//Create a new server object
@@ -93,20 +99,35 @@ func (router *Router) StartProxyService() error {
 	config := &tls.Config{
 		GetCertificate: router.tlsCertManager.GetCert,
 	}
-	ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.ListenPort), config)
-	if err != nil {
-		log.Println(err)
-		return err
-	}
-	router.tlsListener = ln
-	router.server = &http.Server{Addr: ":" + strconv.Itoa(router.ListenPort), Handler: router.mux}
-	router.Running = true
 
-	go func() {
-		if err := router.server.Serve(ln); err != nil && err != http.ErrServerClosed {
-			log.Fatalf("Could not start server: %v\n", err)
+	if router.useTLS {
+		//Serve with TLS mode
+		ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.ListenPort), config)
+		if err != nil {
+			log.Println(err)
+			return err
 		}
-	}()
+		router.tlsListener = ln
+		router.server = &http.Server{Addr: ":" + strconv.Itoa(router.ListenPort), Handler: router.mux}
+		router.Running = true
+
+		log.Println("Reverse proxy service started in the background (TLS mode)")
+		go func() {
+			if err := router.server.Serve(ln); err != nil && err != http.ErrServerClosed {
+				log.Fatalf("Could not start server: %v\n", err)
+			}
+		}()
+	} else {
+		//Serve with non TLS mode
+		router.tlsListener = nil
+		router.server = &http.Server{Addr: ":" + strconv.Itoa(router.ListenPort), Handler: router.mux}
+		router.Running = true
+		log.Println("Reverse proxy service started in the background (Plain HTTP mode)")
+		go func() {
+			router.server.ListenAndServe()
+			//log.Println("[DynamicProxy] " + err.Error())
+		}()
+	}
 
 	return nil
 }
@@ -133,6 +154,22 @@ func (router *Router) StopProxyService() error {
 	return nil
 }
 
+//Restart the current router if it is running.
+//Startup the server if it is not running initially
+func (router *Router) Restart() error {
+	//Stop the router if it is already running
+	if router.Running {
+		err := router.StopProxyService()
+		if err != nil {
+			return err
+		}
+	}
+
+	//Start the server
+	err := router.StartProxyService()
+	return err
+}
+
 /*
 	Add an URL into a custom proxy services
 */

+ 24 - 2
mod/tlscert/tlscert.go

@@ -16,6 +16,7 @@ import (
 
 type Manager struct {
 	CertStore string
+	verbal    bool
 }
 
 func NewManager(certStore string) (*Manager, error) {
@@ -25,6 +26,7 @@ func NewManager(certStore string) (*Manager, error) {
 
 	thisManager := Manager{
 		CertStore: certStore,
+		verbal:    true,
 	}
 
 	return &thisManager, nil
@@ -74,11 +76,21 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
 			//There is a matching parent domain for this subdomain. Use this instead.
 			pubKey = filepath.Join(m.CertStore, cloestDomainCert+".crt")
 			priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
+		} else if m.DefaultCertExists() {
+			//Use default.crt and default.key
+			pubKey = filepath.Join(m.CertStore, "default.crt")
+			priKey = filepath.Join(m.CertStore, "default.key")
+			if m.verbal {
+				log.Println("No matching certificate found. Serving with default")
+			}
 		} else {
-			log.Println("Matching certificate not found. Serving with default. Domain: ", helloInfo.ServerName)
+			if m.verbal {
+				log.Println("Matching certificate not found. Serving with default. Requesting server name: ", helloInfo.ServerName)
+			}
 		}
-
 	}
+
+	//Load the cert and serve it
 	cer, err := tls.LoadX509KeyPair(pubKey, priKey)
 	if err != nil {
 		log.Println(err)
@@ -88,6 +100,16 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
 	return &cer, nil
 }
 
+//Check if both the default cert public key and private key exists
+func (m *Manager) DefaultCertExists() bool {
+	return utils.FileExists(filepath.Join(m.CertStore, "default.crt")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
+}
+
+//Check if the default cert exists returning seperate results for pubkey and prikey
+func (m *Manager) DefaultCertExistsSep() (bool, bool) {
+	return utils.FileExists(filepath.Join(m.CertStore, "default.crt")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
+}
+
 //Check if the given file is a valid TLS file
 func isValidTLSFile(file io.Reader) bool {
 	// Read the contents of the uploaded file

+ 14 - 1
reverseproxy.go

@@ -25,7 +25,20 @@ func ReverseProxtInit() {
 		log.Println("Inbound port not set. Using default (80)")
 	}
 
-	dprouter, err := dynamicproxy.NewDynamicProxy(inboundPort, tlsCertManager)
+	useTls := false
+	if sysdb.KeyExists("settings", "usetls") {
+		sysdb.Read("settings", "usetls", &useTls)
+		if useTls {
+			log.Println("TLS mode enabled. Serving proxxy request with TLS")
+		} else {
+			log.Println("TLS mode disabled. Serving proxy request with plain http")
+		}
+
+	} else {
+		log.Println("Using no TLS for serving proxy domains")
+	}
+
+	dprouter, err := dynamicproxy.NewDynamicProxy(inboundPort, useTls, tlsCertManager)
 	if err != nil {
 		log.Println(err.Error())
 		return

BIN
sys.db


+ 178 - 5
web/components/cert.html

@@ -1,12 +1,183 @@
 
+<h3><i class="ui lock icon"></i> TLS / SSL Certificates</h3>
+<p>Setup TLS cert for different domains of your reverse proxy server names</p>
+<div class="ui divider"></div>
+<h4>Default Certificates</h4>
+    <small>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</small></p>
+    <table class="ui very basic celled table">
+        <thead>
+            <tr><th>Key Type</th>
+            <th>Exists</th>
+        </tr></thead>
+        <tbody>
+            <tr>
+                <td><i class="globe icon"></i> Default Public Key</td>
+                <td id="pubkeyExists"></td>
+            </tr>
+            <tr>
+                <td><i class="lock icon"></i> Default Private Key</td>
+                <td id="prikeyExists"></td>
+            </tr>
+        </tbody>
+    </table>
+<button class="ui button" onclick="uploadPublicKey();"><i class="globe icon"></i> Upload Public Key</button>
+<button class="ui black button" onclick="uploadPrivateKey();"><i class="lock icon"></i> Upload Private Key</button>
+<div class="ui divider"></div>
+<h4>Sub-domain Certificates</h4>
+<p>Provide certificates for multiple domains reverse proxy</p>
+<div class="ui fluid form">
+    <div class="three fields">
+        <div class="field">
+        <label>Server Name (Domain)</label>
+        <input type="text" id="certdomain" placeholder="example.com / blog.example.com">
+        </div>
+        <div class="field">
+            <label>Public Key</label>
+            <input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
+        </div>
+        <div class="field">
+            <label>Private Key</label>
+            <input type="file" id="prikeySelector" onchange="handleFileSelect(event, 'pri')">
+        </div>
+    </div>
+    <button class="ui teal button" onclick="handleDomainKeysUpload();"><i class="ui upload icon"></i> Upload</button>
+</div>
+<div id="certUploadSuccMsg" class="ui green message" style="display:none;">
+    <i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
+</div>
+<br>
+<div >
+    <table class="ui very basic celled table">
+        <thead>
+          <tr><th>Domain</th>
+          <th>Last Update</th>
+          <th>Remove</th>
+        </tr></thead>
+    <tbody id="certifiedDomainList">
 
-<button onclick="uploadPublicKey();">Upload</button>
-<button onclick="uploadPrivateKey();">Upload Private Key</button>
-
+    </tbody>
+    </table>
+</div>
+<div class="ui message">
+    <h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
+    If you have 3rd or even 4th level subdomains like <code>blog.example.com</code> or <code>en.blog.example.com</code> ,
+    depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for <code>a.example.com</code> and <code>b.example.com</code>).<br>
+    If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
+</div>
 <script>
+    var uploadPendingPublicKey = undefined;
+    var uploadPendingPrivateKey = undefined;
+
+    function initManagedDomainCertificateList(){
+        $("#certifiedDomainList").html("");
+        $.get("/cert/list?date=true", function(data){
+            if (data.error != undefined){
+                alert(data.error);
+            }else{
+                data.forEach(entry => {
+                    $("#certifiedDomainList").append(`<tr>
+                    <td>${entry.Domain}</td>
+                    <td>${entry.LastModifiedDate}</td>
+                    <td><button title="Delete key-pair" class="ui mini basic red icon button"><i class="ui red trash icon"></i></button></td>
+                    </tr>`);
+                })
+            }
+        })
+    }
+    initManagedDomainCertificateList();
+
+    function handleDomainUploadByKeypress(){
+        handleDomainKeysUpload(function(){
+            $("#certUploadingDomain").text($("#certdomain").val().trim());
+            //After uploaded, reset the file selector
+            document.getElementById('pubkeySelector').value = '';
+            document.getElementById('prikeySelector').value = '';
+            document.getElementById('certdomain').value = '';
+
+            uploadPendingPublicKey = undefined;
+            uploadPendingPrivateKey = undefined;
+
+            //Show succ
+            $("#certUploadSuccMsg").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
+        });
+    }
+    //Handle domain keys upload
+    function handleDomainKeysUpload(callback=undefined){
+        let domain = $("#certdomain").val();
+        if (domain.trim() == ""){
+            alert("Missing domain.");
+            return;
+        }
+        if (uploadPendingPublicKey && uploadPendingPrivateKey && typeof uploadPendingPublicKey === 'object' && typeof uploadPendingPrivateKey === 'object') {
+            const publicKeyForm = new FormData();
+            publicKeyForm.append('file', uploadPendingPublicKey, 'publicKey');
+
+            const privateKeyForm = new FormData();
+            privateKeyForm.append('file', uploadPendingPrivateKey, 'privateKey');
+
+            const publicKeyRequest = new XMLHttpRequest();
+            publicKeyRequest.open('POST', '/cert/upload?ktype=pub&domain=' + domain);
+            publicKeyRequest.onreadystatechange = function() {
+            if (publicKeyRequest.readyState === XMLHttpRequest.DONE) {
+                if (publicKeyRequest.status !== 200) {
+                    alert('Error uploading public key: ' + publicKeyRequest.statusText);
+                }
+
+                if (callback != undefined){
+                    callback();
+                }
+                
+            }
+            };
+            publicKeyRequest.send(publicKeyForm);
+
+            const privateKeyRequest = new XMLHttpRequest();
+            privateKeyRequest.open('POST', '/cert/upload?ktype=pri&domain=' + domain);
+            privateKeyRequest.onreadystatechange = function() {
+            if (privateKeyRequest.readyState === XMLHttpRequest.DONE) {
+                if (privateKeyRequest.status !== 200) {
+                    alert('Error uploading private key: ' + privateKeyRequest.statusText);
+                }
+                if (callback != undefined){
+                    callback();
+                }
+            }
+            };
+            privateKeyRequest.send(privateKeyForm);
+        } else {
+            alert('One or both of the files is missing or not a file object');
+        }
+    }
+
+    //Handlers for selecting domain based key pairs
+    //ktype = {"pub" / "pri"}
+    function handleFileSelect(event, ktype="pub") {
+        const file = event.target.files[0];
+        //const fileNameInput = document.getElementById('selected-file-name');
+        if (ktype == "pub"){
+            uploadPendingPublicKey = file;
+        }else if (ktype == "pri"){
+            uploadPendingPrivateKey = file;
+        }
+        
+        
+        //fileNameInput.value = file.name;
+    }
+
+    //Check if the default keypairs exists
+    function initDefaultKeypairCheck(){
+        $.get("/cert/checkDefault", function(data){
+            let tick = `<i class="ui green checkmark icon"></i>`;
+            let cross = `<i class="ui red times icon"></i>`;
+            $("#pubkeyExists").html(data.DefaultPubExists?tick:cross);
+            $("#prikeyExists").html(data.DefaultPriExists?tick:cross);
+        });
+    }
+    initDefaultKeypairCheck();
+
     function uploadPrivateKey(){
-  // create file input element
-  const input = document.createElement('input');
+        // create file input element
+        const input = document.createElement('input');
         input.type = 'file';
         
         // add change listener to file input
@@ -23,6 +194,7 @@
                 body: formData
             })
             .then(response => {
+                initDefaultKeypairCheck();
                 if (response.ok) {
                     alert('File upload successful!');
                 } else {
@@ -64,6 +236,7 @@
             .then(response => {
                 if (response.ok) {
                     alert('File upload successful!');
+                    initDefaultKeypairCheck();
                 } else {
                     response.text().then(text => {
                         alert(text);

+ 34 - 0
web/components/status.html

@@ -13,6 +13,12 @@
     <input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
     <button class="ui button" onclick="handlePortChange();">Apply</button>
 </div>
+<br>
+<div id="tls" class="ui toggle checkbox">
+    <input type="checkbox">
+    <label>Use TLS to serve proxy request</label>
+</div>
+<br>
 <div id="portUpdateSucc" class="ui green message" style="display:none;">
     <i class="ui green checkmark icon"></i> Setting Updated
 </div>
@@ -76,4 +82,32 @@
         });
     }
 
+    function initTlsSetting(){
+        $.get("/cert/tls", function(data){
+            if (data == true){
+                $("#tls").checkbox("set checked");
+            }
+
+            //Initiate the input listener on the checkbox
+            $("#tls").find("input").on("change", function(){
+                let thisValue = $("#tls").checkbox("is checked");
+                $.ajax({
+                    url: "/cert/tls",
+                    data: {set: thisValue},
+                    success: function(data){
+                        if (data.error != undefined){
+                            alert(data.error);
+                        }else{
+                            //Updated
+                            $("#portUpdateSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
+                            initRPStaste();
+                        }
+                    }
+                })
+            });
+        })
+      
+    }
+    initTlsSetting();
+
 </script>