Move handlers into separate package
This commit is contained in:
		
							parent
							
								
									b748050de3
								
							
						
					
					
						commit
						08be6e68bc
					
				
					 4 changed files with 393 additions and 325 deletions
				
			
		
							
								
								
									
										98
									
								
								handlers/index.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								handlers/index.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,98 @@ | |||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"html/template" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/gorilla/csrf" | ||||
| 	"github.com/nicksnyder/go-i18n/v2/i18n" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
| 
 | ||||
| type IndexHandler struct { | ||||
| 	bundle *i18n.Bundle | ||||
| } | ||||
| 
 | ||||
| func NewIndexHandler(bundle *i18n.Bundle) *IndexHandler { | ||||
| 	return &IndexHandler{bundle: bundle} | ||||
| } | ||||
| 
 | ||||
| func (i *IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	localizer := i18n.NewLocalizer(i.bundle, r.Header.Get("Accept-Language")) | ||||
| 	csrGenTitle := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "CSRGenTitle", | ||||
| 		Other: "CSR generation in browser", | ||||
| 	}}) | ||||
| 	nameLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "NameLabel", | ||||
| 		Other: "Your name", | ||||
| 	}}) | ||||
| 	nameHelpText := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "NameHelpText", | ||||
| 		Other: "Please input your name as it should be added to your certificate", | ||||
| 	}}) | ||||
| 	passwordLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "PasswordLabel", | ||||
| 		Other: "Password for your client certificate", | ||||
| 	}}) | ||||
| 	rsaKeySizeLegend := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSAKeySizeLabel", | ||||
| 		Other: "RSA Key Size", | ||||
| 	}}) | ||||
| 	rsa3072Label := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSA3072Label", | ||||
| 		Other: "3072 Bit", | ||||
| 	}}) | ||||
| 	rsa2048Label := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSA2048Label", | ||||
| 		Other: "2048 Bit (not recommended)", | ||||
| 	}}) | ||||
| 	rsa4096Label := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "RSA4096Label", | ||||
| 		Other: "4096 Bit", | ||||
| 	}}) | ||||
| 	rsaHelpText := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID: "RSAHelpText", | ||||
| 		Other: "An RSA key pair will be generated in your browser. Longer key" + | ||||
| 			" sizes provide better security but take longer to generate.", | ||||
| 	}}) | ||||
| 	csrButtonLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "CSRButtonLabel", | ||||
| 		Other: "Generate signing request", | ||||
| 	}}) | ||||
| 	statusLoading := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "StatusLoading", | ||||
| 		Other: "Loading ...", | ||||
| 	}}) | ||||
| 	downloadLabel := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "DownloadLabel", | ||||
| 		Other: "Download", | ||||
| 	}}) | ||||
| 	downloadDescription := localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID: "DownloadDescription", | ||||
| 		Other: "Your key material is ready for download. The downloadable file contains your private key and your" + | ||||
| 			" certificate encrypted with your password. You can now use the file to install your certificate in your" + | ||||
| 			" browser or other applications.", | ||||
| 	}}) | ||||
| 
 | ||||
| 	t := template.Must(template.ParseFiles("templates/index.html")) | ||||
| 	err := t.Execute(w, map[string]interface{}{ | ||||
| 		"Title":               csrGenTitle, | ||||
| 		"NameLabel":           nameLabel, | ||||
| 		"NameHelpText":        nameHelpText, | ||||
| 		"PasswordLabel":       passwordLabel, | ||||
| 		"RSAKeySizeLegend":    rsaKeySizeLegend, | ||||
| 		"RSA3072Label":        rsa3072Label, | ||||
| 		"RSA2048Label":        rsa2048Label, | ||||
| 		"RSA4096Label":        rsa4096Label, | ||||
| 		"RSAHelpText":         rsaHelpText, | ||||
| 		"CSRButtonLabel":      csrButtonLabel, | ||||
| 		"StatusLoading":       statusLoading, | ||||
| 		"DownloadDescription": downloadDescription, | ||||
| 		"DownloadLabel":       downloadLabel, | ||||
| 		csrf.TemplateTag:      csrf.TemplateField(r), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		log.Panic(err) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										55
									
								
								handlers/jslocales.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								handlers/jslocales.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | |||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/nicksnyder/go-i18n/v2/i18n" | ||||
| ) | ||||
| 
 | ||||
| type JSLocalesHandler struct { | ||||
| 	bundle *i18n.Bundle | ||||
| } | ||||
| 
 | ||||
| func NewJSLocalesHandler(bundle *i18n.Bundle) *JSLocalesHandler { | ||||
| 	return &JSLocalesHandler{bundle: bundle} | ||||
| } | ||||
| 
 | ||||
| func (j *JSLocalesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	parts := strings.Split(r.URL.Path, "/") | ||||
| 	if len(parts) != 4 { | ||||
| 		http.Error(w, "Not found", http.StatusNotFound) | ||||
| 		return | ||||
| 	} | ||||
| 	lang := parts[2] | ||||
| 
 | ||||
| 	localizer := i18n.NewLocalizer(j.bundle, lang) | ||||
| 
 | ||||
| 	type translationData struct { | ||||
| 		Keygen struct { | ||||
| 			Started   string `json:"started"` | ||||
| 			Running   string `json:"running"` | ||||
| 			Generated string `json:"generated"` | ||||
| 		} `json:"keygen"` | ||||
| 	} | ||||
| 
 | ||||
| 	translations := &translationData{} | ||||
| 	translations.Keygen.Started = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "JavaScript.KeyGen.Started", | ||||
| 		Other: "started key generation", | ||||
| 	}}) | ||||
| 	translations.Keygen.Running = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "JavaScript.KeyGen.Running", | ||||
| 		Other: "key generation running for __seconds__ seconds", | ||||
| 	}}) | ||||
| 	translations.Keygen.Generated = localizer.MustLocalize(&i18n.LocalizeConfig{DefaultMessage: &i18n.Message{ | ||||
| 		ID:    "JavaScript.KeyGen.Generated", | ||||
| 		Other: "key generated in __seconds__ seconds", | ||||
| 	}}) | ||||
| 
 | ||||
| 	encoder := json.NewEncoder(w) | ||||
| 	if err := encoder.Encode(translations); err != nil { | ||||
| 		http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										171
									
								
								handlers/signing.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								handlers/signing.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,171 @@ | |||
| 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 | ||||
| } | ||||
| 
 | ||||
| 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 (h *CertificateSigningHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	if r.Method != "POST" { | ||||
| 		http.Error(w, "Only POST requests support", http.StatusMethodNotAllowed) | ||||
| 		return | ||||
| 	} | ||||
| 	if r.Header.Get("content-type") != "application/json" { | ||||
| 		http.Error(w, "Only JSON content is accepted", http.StatusNotAcceptable) | ||||
| 		return | ||||
| 	} | ||||
| 	var err error | ||||
| 	var requestBody requestData | ||||
| 	var responseData responseData | ||||
| 
 | ||||
| 	if err = json.NewDecoder(r.Body).Decode(&requestBody); err != nil { | ||||
| 		log.Print(err) | ||||
| 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	responseData.Certificate, responseData.CAChain, err = h.sign(requestBody.Csr, requestBody.CommonName) | ||||
| 	if err != nil { | ||||
| 		http.Error(w, "Could not sign certificate", http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var jsonBytes []byte | ||||
| 	if jsonBytes, err = json.Marshal(&responseData); err != nil { | ||||
| 		log.Print(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err = w.Write(jsonBytes); err != nil { | ||||
| 		log.Print(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type requestData struct { | ||||
| 	Csr        string `json:"csr"` | ||||
| 	CommonName string `json:"commonName"` | ||||
| } | ||||
| 
 | ||||
| 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++ | ||||
| 	} | ||||
| } | ||||
		Reference in a new issue