Decouple request and response via WebSocket
This commit is contained in:
		
							parent
							
								
									08be6e68bc
								
							
						
					
					
						commit
						2093bf2429
					
				
					 12 changed files with 369 additions and 146 deletions
				
			
		|  | @ -18,6 +18,15 @@ func NewIndexHandler(bundle *i18n.Bundle) *IndexHandler { | |||
| } | ||||
| 
 | ||||
| func (i *IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	if r.Method != "GET" { | ||||
| 		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) | ||||
| 		return | ||||
| 	} | ||||
| 	if r.URL.Path != "/" { | ||||
| 		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	localizer := i18n.NewLocalizer(i.bundle, r.Header.Get("Accept-Language")) | ||||
| 	csrGenTitle := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "CSRGenTitle", | ||||
|  |  | |||
|  | @ -32,6 +32,10 @@ func (j *JSLocalesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
| 			Running   string `json:"running"` | ||||
| 			Generated string `json:"generated"` | ||||
| 		} `json:"keygen"` | ||||
| 		Certificate struct { | ||||
| 			Waiting  string `json:"waiting"` | ||||
| 			Received string `json:"received"` | ||||
| 		} `json:"certificate"` | ||||
| 	} | ||||
| 
 | ||||
| 	translations := &translationData{} | ||||
|  | @ -47,6 +51,14 @@ func (j *JSLocalesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
| 		ID:    "JavaScript.KeyGen.Generated", | ||||
| 		Other: "key generated in __seconds__ seconds", | ||||
| 	}}) | ||||
| 	translations.Certificate.Waiting = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "JavaScript.Certificate.Waiting", | ||||
| 		Other: "waiting for certificate ...", | ||||
| 	}}) | ||||
| 	translations.Certificate.Received = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "JavaScript.Certificate.Received", | ||||
| 		Other: "received certificate from CA", | ||||
| 	}}) | ||||
| 
 | ||||
| 	encoder := json.NewEncoder(w) | ||||
| 	if err := encoder.Encode(translations); err != nil { | ||||
|  |  | |||
							
								
								
									
										172
									
								
								handlers/registry.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								handlers/registry.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,172 @@ | |||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/pem" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/google/uuid" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
| 
 | ||||
| type SigningRequestRegistry struct { | ||||
| 	caCertificates []*x509.Certificate | ||||
| 	caChainMap     map[string][]string | ||||
| 	requests       map[string]chan *responseData | ||||
| } | ||||
| 
 | ||||
| func NewSigningRequestRegistry(caCertificates []*x509.Certificate) *SigningRequestRegistry { | ||||
| 	return &SigningRequestRegistry{ | ||||
| 		caCertificates: caCertificates, | ||||
| 		caChainMap:     make(map[string][]string), | ||||
| 		requests:       make(map[string]chan *responseData), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (registry *SigningRequestRegistry) AddSigningRequest(request *requestData) (string, error) { | ||||
| 	requestUuid, err := uuid.NewRandom() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	go func() { | ||||
| 		responseChannel := make(chan *responseData, 1) | ||||
| 		registry.requests[requestUuid.String()] = responseChannel | ||||
| 		registry.signCertificate(responseChannel, request) | ||||
| 	}() | ||||
| 	return requestUuid.String(), nil | ||||
| } | ||||
| 
 | ||||
| func (registry *SigningRequestRegistry) signCertificate(channel chan *responseData, request *requestData) { | ||||
| 	responseData, err := registry.sign(request) | ||||
| 	if err != nil { | ||||
| 		log.Error(err) | ||||
| 		close(channel) | ||||
| 		return | ||||
| 	} | ||||
| 	channel <- responseData | ||||
| } | ||||
| 
 | ||||
| func (registry *SigningRequestRegistry) sign(request *requestData) (response *responseData, err error) { | ||||
| 	log.Debugf("received CSR for %s:\n\n%s", request.CommonName, request.Csr) | ||||
| 	subjectDN := fmt.Sprintf("/CN=%s", request.CommonName) | ||||
| 	var csrFile *os.File | ||||
| 	if csrFile, err = ioutil.TempFile("", "*.csr.pem"); err != nil { | ||||
| 		log.Errorf("could not open temporary file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if _, err = csrFile.Write([]byte(request.Csr)); err != nil { | ||||
| 		log.Errorf("could not write CSR to file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if err = csrFile.Close(); err != nil { | ||||
| 		log.Errorf("could not close CSR file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	defer func(file *os.File) { | ||||
| 		err = os.Remove(file.Name()) | ||||
| 		if err != nil { | ||||
| 			log.Errorf("could not remove temporary file: %s", err) | ||||
| 		} | ||||
| 	}(csrFile) | ||||
| 
 | ||||
| 	// simulate a delay during certificate creation | ||||
| 	time.Sleep(5 * time.Second) | ||||
| 
 | ||||
| 	opensslCommand := exec.Command( | ||||
| 		"openssl", "ca", "-config", "ca.cnf", | ||||
| 		"-policy", "policy_match", "-extensions", "client_ext", | ||||
| 		"-batch", "-subj", subjectDN, "-utf8", "-rand_serial", "-in", "in.pem") | ||||
| 	var out, cmdErr bytes.Buffer | ||||
| 	opensslCommand.Stdout = &out | ||||
| 	opensslCommand.Stderr = &cmdErr | ||||
| 	err = opensslCommand.Run() | ||||
| 	if err != nil { | ||||
| 		log.Error(err) | ||||
| 		log.Error(cmdErr.String()) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var block *pem.Block | ||||
| 	if block, _ = pem.Decode(out.Bytes()); block == nil { | ||||
| 		err = fmt.Errorf("could not decode pem") | ||||
| 		return | ||||
| 	} | ||||
| 	var certificate *x509.Certificate | ||||
| 	if certificate, err = x509.ParseCertificate(block.Bytes); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var caChain []string | ||||
| 	if caChain, err = registry.getCAChain(certificate); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	response = &responseData{ | ||||
| 		Certificate: string(pem.EncodeToMemory(&pem.Block{ | ||||
| 			Type:  "CERTIFICATE", | ||||
| 			Bytes: certificate.Raw, | ||||
| 		})), | ||||
| 		CAChain: caChain, | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (registry *SigningRequestRegistry) GetResponseChannel(requestUuid string) (chan *responseData, error) { | ||||
| 	if responseChannel, exists := registry.requests[requestUuid]; exists { | ||||
| 		delete(registry.requests, requestUuid) | ||||
| 		return responseChannel, nil | ||||
| 	} else { | ||||
| 		return nil, errors.New("no request found") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (registry *SigningRequestRegistry) getCAChain(certificate *x509.Certificate) ([]string, error) { | ||||
| 	issuerString := string(certificate.RawIssuer) | ||||
| 
 | ||||
| 	if value, exists := registry.caChainMap[issuerString]; exists { | ||||
| 		return value, nil | ||||
| 	} | ||||
| 
 | ||||
| 	result := make([]string, 0) | ||||
| 
 | ||||
| 	appendCert := func(cert *x509.Certificate) { | ||||
| 		result = append( | ||||
| 			result, | ||||
| 			string(pem.EncodeToMemory(&pem.Block{Bytes: cert.Raw, Type: "CERTIFICATE"}))) | ||||
| 		log.Debugf("added %s to cachain", result[len(result)-1]) | ||||
| 	} | ||||
| 
 | ||||
| 	var previous *x509.Certificate | ||||
| 	var count = 0 | ||||
| 	for { | ||||
| 		if len(registry.caCertificates) == 0 { | ||||
| 			return nil, errors.New("no CA certificates loaded") | ||||
| 		} | ||||
| 		if count > len(registry.caCertificates) { | ||||
| 			return nil, errors.New("could not construct certificate chain") | ||||
| 		} | ||||
| 		for _, caCert := range registry.caCertificates { | ||||
| 			if previous == nil { | ||||
| 				if bytes.Equal(caCert.RawSubject, certificate.RawIssuer) { | ||||
| 					previous = caCert | ||||
| 					appendCert(caCert) | ||||
| 				} | ||||
| 			} else if bytes.Equal(previous.RawSubject, previous.RawIssuer) { | ||||
| 				registry.caChainMap[issuerString] = result | ||||
| 				return result, nil | ||||
| 			} else if bytes.Equal(caCert.RawSubject, previous.RawIssuer) { | ||||
| 				previous = caCert | ||||
| 				appendCert(caCert) | ||||
| 			} else { | ||||
| 				log.Debugf("skipped certificate %s", caCert.Subject) | ||||
| 			} | ||||
| 		} | ||||
| 		count++ | ||||
| 	} | ||||
| } | ||||
|  | @ -1,83 +1,18 @@ | |||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/json" | ||||
| 	"encoding/pem" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 
 | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
| 
 | ||||
| type CertificateSigningHandler struct { | ||||
| 	caCertificates []*x509.Certificate | ||||
| 	caChainMap     map[string][]string | ||||
| 	requestRegistry *SigningRequestRegistry | ||||
| } | ||||
| 
 | ||||
| func NewCertificateSigningHandler(caCertificates []*x509.Certificate) *CertificateSigningHandler { | ||||
| 	return &CertificateSigningHandler{caCertificates: caCertificates, caChainMap: make(map[string][]string)} | ||||
| } | ||||
| 
 | ||||
| func (h *CertificateSigningHandler) sign(csrPem string, commonName string) (certPem string, caChain []string, err error) { | ||||
| 	log.Printf("received CSR for %s:\n\n%s", commonName, csrPem) | ||||
| 	subjectDN := fmt.Sprintf("/CN=%s", commonName) | ||||
| 	var csrFile *os.File | ||||
| 	if csrFile, err = ioutil.TempFile("", "*.csr.pem"); err != nil { | ||||
| 		log.Errorf("could not open temporary file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if _, err = csrFile.Write([]byte(csrPem)); err != nil { | ||||
| 		log.Errorf("could not write CSR to file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if err = csrFile.Close(); err != nil { | ||||
| 		log.Errorf("could not close CSR file: %s", err) | ||||
| 		return | ||||
| 	} | ||||
| 	defer func(file *os.File) { | ||||
| 		err = os.Remove(file.Name()) | ||||
| 		if err != nil { | ||||
| 			log.Errorf("could not remove temporary file: %s", err) | ||||
| 		} | ||||
| 	}(csrFile) | ||||
| 
 | ||||
| 	opensslCommand := exec.Command( | ||||
| 		"openssl", "ca", "-config", "ca.cnf", | ||||
| 		"-policy", "policy_match", "-extensions", "client_ext", | ||||
| 		"-batch", "-subj", subjectDN, "-utf8", "-rand_serial", "-in", "in.pem") | ||||
| 	var out, cmdErr bytes.Buffer | ||||
| 	opensslCommand.Stdout = &out | ||||
| 	opensslCommand.Stderr = &cmdErr | ||||
| 	err = opensslCommand.Run() | ||||
| 	if err != nil { | ||||
| 		log.Print(err) | ||||
| 		log.Print(cmdErr.String()) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var block *pem.Block | ||||
| 	if block, _ = pem.Decode(out.Bytes()); block == nil { | ||||
| 		err = fmt.Errorf("could not decode pem") | ||||
| 		return | ||||
| 	} | ||||
| 	var certificate *x509.Certificate | ||||
| 	if certificate, err = x509.ParseCertificate(block.Bytes); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	certPem = string(pem.EncodeToMemory(&pem.Block{ | ||||
| 		Type:  "CERTIFICATE", | ||||
| 		Bytes: certificate.Raw, | ||||
| 	})) | ||||
| 
 | ||||
| 	caChain, err = h.getCAChain(certificate) | ||||
| 	return | ||||
| func NewCertificateSigningHandler(requestRegistry *SigningRequestRegistry) *CertificateSigningHandler { | ||||
| 	return &CertificateSigningHandler{requestRegistry: requestRegistry} | ||||
| } | ||||
| 
 | ||||
| func (h *CertificateSigningHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
|  | @ -91,26 +26,27 @@ func (h *CertificateSigningHandler) ServeHTTP(w http.ResponseWriter, r *http.Req | |||
| 	} | ||||
| 	var err error | ||||
| 	var requestBody requestData | ||||
| 	var responseData responseData | ||||
| 
 | ||||
| 	if err = json.NewDecoder(r.Body).Decode(&requestBody); err != nil { | ||||
| 		log.Print(err) | ||||
| 		log.Error(err) | ||||
| 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	responseData.Certificate, responseData.CAChain, err = h.sign(requestBody.Csr, requestBody.CommonName) | ||||
| 	type acceptedResponse struct { | ||||
| 		RequestId string `json:"request_id"` | ||||
| 	} | ||||
| 
 | ||||
| 	taskUuid, err := h.requestRegistry.AddSigningRequest(&requestBody) | ||||
| 	if err != nil { | ||||
| 		http.Error(w, "Could not sign certificate", http.StatusInternalServerError) | ||||
| 		log.Error(err) | ||||
| 		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var jsonBytes []byte | ||||
| 	if jsonBytes, err = json.Marshal(&responseData); err != nil { | ||||
| 		log.Print(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err = w.Write(jsonBytes); err != nil { | ||||
| 	w.WriteHeader(http.StatusAccepted) | ||||
| 	response := &acceptedResponse{RequestId: taskUuid} | ||||
| 	if err = json.NewEncoder(w).Encode(response); err != nil { | ||||
| 		log.Print(err) | ||||
| 	} | ||||
| } | ||||
|  | @ -124,48 +60,3 @@ type responseData struct { | |||
| 	Certificate string   `json:"certificate"` | ||||
| 	CAChain     []string `json:"ca_chain"` | ||||
| } | ||||
| 
 | ||||
| func (h *CertificateSigningHandler) getCAChain(certificate *x509.Certificate) ([]string, error) { | ||||
| 	issuerString := string(certificate.RawIssuer) | ||||
| 
 | ||||
| 	if value, exists := h.caChainMap[issuerString]; exists { | ||||
| 		return value, nil | ||||
| 	} | ||||
| 
 | ||||
| 	result := make([]string, 0) | ||||
| 
 | ||||
| 	appendCert := func(cert *x509.Certificate) { | ||||
| 		result = append( | ||||
| 			result, | ||||
| 			string(pem.EncodeToMemory(&pem.Block{Bytes: cert.Raw, Type: "CERTIFICATE"}))) | ||||
| 		log.Debugf("added %s to cachain", result[len(result)-1]) | ||||
| 	} | ||||
| 
 | ||||
| 	var previous *x509.Certificate | ||||
| 	var count = 0 | ||||
| 	for { | ||||
| 		if len(h.caCertificates) == 0 { | ||||
| 			return nil, errors.New("no CA certificates loaded") | ||||
| 		} | ||||
| 		if count > len(h.caCertificates) { | ||||
| 			return nil, errors.New("could not construct certificate chain") | ||||
| 		} | ||||
| 		for _, caCert := range h.caCertificates { | ||||
| 			if previous == nil { | ||||
| 				if bytes.Equal(caCert.RawSubject, certificate.RawIssuer) { | ||||
| 					previous = caCert | ||||
| 					appendCert(caCert) | ||||
| 				} | ||||
| 			} else if bytes.Equal(previous.RawSubject, previous.RawIssuer) { | ||||
| 				h.caChainMap[issuerString] = result | ||||
| 				return result, nil | ||||
| 			} else if bytes.Equal(caCert.RawSubject, previous.RawIssuer) { | ||||
| 				previous = caCert | ||||
| 				appendCert(caCert) | ||||
| 			} else { | ||||
| 				log.Debugf("skipped certificate %s", caCert.Subject) | ||||
| 			} | ||||
| 		} | ||||
| 		count++ | ||||
| 	} | ||||
| } | ||||
|  |  | |||
							
								
								
									
										79
									
								
								handlers/websocket.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								handlers/websocket.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | |||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gobwas/ws" | ||||
| 	"github.com/gobwas/ws/wsutil" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
| 
 | ||||
| type WebSocketHandler struct { | ||||
| 	requestRegistry *SigningRequestRegistry | ||||
| } | ||||
| 
 | ||||
| func NewWebSocketHandler(registry *SigningRequestRegistry) *WebSocketHandler { | ||||
| 	return &WebSocketHandler{requestRegistry: registry} | ||||
| } | ||||
| 
 | ||||
| func (w *WebSocketHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { | ||||
| 	conn, _, _, err := ws.UpgradeHTTP(request, writer) | ||||
| 	if err != nil { | ||||
| 		http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer func() { _ = conn.Close() }() | ||||
| 
 | ||||
| 		var ( | ||||
| 			reader      = wsutil.NewReader(conn, ws.StateServerSide) | ||||
| 			writer      = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) | ||||
| 			jsonDecoder = json.NewDecoder(reader) | ||||
| 			jsonEncoder = json.NewEncoder(writer) | ||||
| 		) | ||||
| 
 | ||||
| 		for { | ||||
| 			header, err := reader.NextFrame() | ||||
| 			if err != nil { | ||||
| 				log.Error(err) | ||||
| 				break | ||||
| 			} | ||||
| 			if header.OpCode == ws.OpClose { | ||||
| 				log.Debug("channel closed") | ||||
| 				break | ||||
| 			} | ||||
| 
 | ||||
| 			type requestType struct { | ||||
| 				RequestId string `json:"request_id"` | ||||
| 			} | ||||
| 
 | ||||
| 			request := &requestType{} | ||||
| 			err = jsonDecoder.Decode(request) | ||||
| 			if err != nil { | ||||
| 				log.Error(err) | ||||
| 				break | ||||
| 			} | ||||
| 
 | ||||
| 			channel, err := w.requestRegistry.GetResponseChannel(request.RequestId) | ||||
| 			if err != nil { | ||||
| 				log.Error(err) | ||||
| 				break | ||||
| 			} | ||||
| 
 | ||||
| 			var response *responseData | ||||
| 			response = <-channel | ||||
| 			if err = jsonEncoder.Encode(response); err != nil { | ||||
| 				log.Error(err) | ||||
| 				break | ||||
| 			} | ||||
| 			close(channel) | ||||
| 
 | ||||
| 			if err = writer.Flush(); err != nil { | ||||
| 				log.Error(err) | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| } | ||||
		Reference in a new issue