Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ pids

# Coverage directory used by tools like istanbul
coverage
*.lcov
*.lcov

# vscode
.vscode/launch.json
2 changes: 0 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ linters:
- bidichk # Checks for dangerous unicode character sequences
# - bodyclose # checks whether HTTP response body is closed successfully
# - contextcheck # check the function whether use a non-inherited context
- deadcode # Finds unused code
- decorder # check declaration order and count of types, constants, variables and functions
# - depguard # Go linter that checks if package imports are in a list of acceptable packages
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
Expand Down Expand Up @@ -74,7 +73,6 @@ linters:
- unconvert # Remove unnecessary type conversions
- unparam # Reports unused function parameters
- unused # Checks Go code for unused constants, variables, functions and types
- varcheck # Finds unused global variables and constants
# - wastedassign # wastedassign finds wasted assignment statements
- whitespace # Tool for detection of leading and trailing whitespace
- makezero # Finds slice declarations with non-zero initial length
Expand Down
2 changes: 1 addition & 1 deletion cmd/defaultConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func resolveDefaultConfig(configPath string) error {
return nil
}
configDirectoryPath := filepath.Dir(configPath)
cfg := config.DefaultConfig(configDirectoryPath + "/www")
cfg := config.DefaultConfig(configDirectoryPath)
if err := os.WriteFile(configPath, []byte(cfg.String()), 0o600); err != nil {
return fmt.Errorf("cannot write default config: %w", err)
}
Expand Down
172 changes: 172 additions & 0 deletions cmd/generateSelfSigned.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package main

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"os"
"path"
"strings"
"time"

"github.com/apex/log"
externalip "github.com/glendc/go-external-ip"
)

func getExternalIP() (net.IP, error) {
consensus := externalip.DefaultConsensus(&externalip.ConsensusConfig{
Timeout: time.Second * 3,
}, nil)
return consensus.ExternalIP()
}

func getExternalDomains(ip net.IP) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
dns, err := net.DefaultResolver.LookupAddr(ctx, ip.String())
if err != nil {
return nil, err
}
// remove trailing dot
for i := range dns {
dns[i] = strings.TrimSuffix(dns[i], ".")
}
return dns, nil
}

func setupIPsFromInterface(template *x509.Certificate, iface net.Interface) {
if iface.Flags&net.FlagLoopback != 0 {
return
}
if iface.Flags&net.FlagUp == 0 {
return
}
addrs, err := iface.Addrs()
if err != nil {
log.Warnf("cannot get network interface(%v) addresses: %v", iface.Name, err)
return
}
for _, addr := range addrs {
if ip, _, err3 := net.ParseCIDR(addr.String()); err3 == nil && !ip.IsUnspecified() {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
log.Warnf("cannot parse network interface(%v) address(%v): %v", iface.Name, addr.String(), err3)
}
}
}

func setupIPsFromInterfaces(template *x509.Certificate) {
ifaces, err := net.Interfaces()
if err != nil {
log.Warnf("cannot get network interfaces: %v", err)
return
}
for _, iface := range ifaces {
setupIPsFromInterface(template, iface)
}
}

func setupIPAndDomains(template *x509.Certificate) {
template.DNSNames = []string{"localhost"}
template.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}
if ip, err := getExternalIP(); err != nil {
log.Warnf("cannot get external IP: %v", err)
} else {
template.IPAddresses = append(template.IPAddresses, ip)
domains, err := getExternalDomains(ip)
if err != nil {
log.Warnf("cannot get external domains: %v", err)
} else {
template.DNSNames = append(template.DNSNames, domains...)
}
}
setupIPsFromInterfaces(template)
}

const cn = "self-signed-certificate"

func checkSelfSignedCertificate(certFile, keyFile string) bool {
crt, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return false
}
cert, err := x509.ParseCertificate(crt.Certificate[0])
if err != nil {
return false
}
if cert.Subject.CommonName != cn {
// it is not self signed certificate
return true
}
now := time.Now()
if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
return false
}
return true
}

func generateSelfSigned(certFile, keyFile string) error {
err := os.MkdirAll(path.Dir(certFile), 0o700)
if err != nil {
return fmt.Errorf("cannot create directory(%v) for certificate: %w", path.Dir(certFile), err)
}
err = os.MkdirAll(path.Dir(keyFile), 0o700)
if err != nil {
return fmt.Errorf("cannot create directory(%v) for key: %w", path.Dir(keyFile), err)
}
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return fmt.Errorf("cannot generate key: %w", err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"plgd.dev"},
CommonName: cn,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365),

KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: true,
}
setupIPAndDomains(&template)

derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return fmt.Errorf("cannot create certificate: %w", err)
}

derKeyBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return fmt.Errorf("cannot marshal private key: %w", err)
}

err = os.WriteFile(certFile, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: derBytes,
}), 0o600)
if err != nil {
return fmt.Errorf("cannot write certificate to file(%v): %w", certFile, err)
}

err = os.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: derKeyBytes,
}), 0o600)
if err != nil {
return fmt.Errorf("cannot write key to file(%v): %w", keyFile, err)
}

return nil
}
35 changes: 25 additions & 10 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,42 +37,57 @@ var (
ReleaseURL = "unknown url"
)

func main() {
func loadConfig() config.Config {
var opts struct {
Version bool `short:"v" long:"version" description:"version"`
ConfigPath string `long:"config" description:"yaml config file path"`
}
_, _ = flags.NewParser(&opts, flags.Default|flags.IgnoreUnknown).Parse()
if opts.Version {
fmt.Println(Version)
return
os.Exit(0)
}
if err := resolveDefaultConfig(opts.ConfigPath); err != nil {
log.Errorf("cannot create default config: %v", err)
return
os.Exit(1)
}
// parse line arguments again because resolveDefaultConfig can set config path
_, _ = flags.NewParser(&opts, flags.Default|flags.IgnoreUnknown).Parse()
cfg, err := config.New(opts.ConfigPath)
if err != nil {
log.Errorf("cannot load config: %v", err)
return
os.Exit(1)
}
if _, err = os.Stat(cfg.APIs.HTTP.UI.Directory); cfg.APIs.HTTP.UI.Enabled && err != nil {
if err = extractUI(cfg.APIs.HTTP.UI.Directory); err != nil {
log.Errorf("cannot extract UI: %v", err)
os.Exit(1)
}
}
if cfg.APIs.HTTP.Enabled && cfg.APIs.HTTP.TLS.Enabled && !checkSelfSignedCertificate(cfg.APIs.HTTP.TLS.CertFile, cfg.APIs.HTTP.TLS.KeyFile) {
if err = generateSelfSigned(cfg.APIs.HTTP.TLS.CertFile, cfg.APIs.HTTP.TLS.KeyFile); err != nil {
log.Errorf("cannot generate self signed certificate for HTTP: %v", err)
os.Exit(1)
}
}
if cfg.APIs.GRPC.Enabled && cfg.APIs.GRPC.TLS.Enabled && !checkSelfSignedCertificate(cfg.APIs.GRPC.TLS.CertFile, cfg.APIs.GRPC.TLS.KeyFile) {
if err = generateSelfSigned(cfg.APIs.GRPC.TLS.CertFile, cfg.APIs.GRPC.TLS.KeyFile); err != nil {
log.Errorf("cannot generate self signed certificate for GRPC: %v", err)
os.Exit(1)
}
}
return cfg
}

func main() {
cfg := loadConfig()
logger := log.NewLogger(cfg.Log)
log.Set(logger)
fileWatcher, err := fsnotify.NewWatcher(logger)
if err != nil {
log.Errorf("cannot create file fileWatcher: %v", err)
return
os.Exit(1)
}
defer func() {
_ = fileWatcher.Close()
}()
log.Debugf("version: %v, buildDate: %v, buildRevision %v", Version, BuildDate, CommitHash)
log.Debugf("config:\n%v", cfg.String())
info := grpc.ServiceInformation{
Expand All @@ -86,11 +101,11 @@ func main() {
s, err := service.New(context.Background(), cfg, &info, fileWatcher, logger)
if err != nil {
log.Errorf("cannot create service: %v", err)
return
os.Exit(1)
}
err = s.Serve()
if err != nil {
log.Errorf("cannot serve service: %v", err)
return
os.Exit(1)
}
}
18 changes: 9 additions & 9 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ apis:
idleTimeout: 30s
tls:
enabled: false
caPool: /certs/ca.pem
keyFile: /certs/key.pem
certFile: /certs/crt.pem
caPool: certs/ca.pem
keyFile: certs/key.pem
certFile: certs/crt.pem
clientCertificateRequired: true
cors:
allowedOrigins:
Expand Down Expand Up @@ -59,9 +59,9 @@ apis:
timeout: 0s
tls:
enabled: false
caPool: /certs/ca.pem
keyFile: /certs/key.pem
certFile: /certs/crt.pem
caPool: certs/ca.pem
keyFile: certs/key.pem
certFile: certs/crt.pem
clientCertificateRequired: true
clients:
device:
Expand All @@ -77,9 +77,9 @@ clients:
- justWorks
manufacturerCertificate:
tls:
caPool: /certs/mfg_ca.pem
keyFile: /certs/mfg_key.pem
certFile: /certs/mfg_crt.pem
caPool: certs/mfg_ca.pem
keyFile: certs/mfg_key.pem
certFile: certs/mfg_crt.pem
tls:
authentication: preSharedKey
preSharedKey:
Expand Down
Loading