diff --git a/.env.example b/.env.example index 0bef351..8a43403 100644 --- a/.env.example +++ b/.env.example @@ -10,10 +10,17 @@ GRID_KEY_PATH=/home/mytkom/.globus/userkey.pem CERN_REDIRECT_URL=http://localhost:8088/callback CERN_REALM_URL=https://auth.cern.ch/auth/realms/cern -CCDB_URL=http://ccdb-test.cern.ch:8080 +CCDB_URL=https://alice-ccdb.cern.ch CCDB_UPLOAD_SUBDIR=Users/m/mmytkows/test +# Dir (or single file) with CERN Root CA 2 + CERN Grid CA (1). Both needed: +# alice-ccdb leaf is signed by Grid CA; server often omits that intermediate. +# mkdir -p certs && curl -fsSL -o certs/cern-root-ca-2.crt \ +# "https://ca.cern.ch/cafiles/certificates/CERN%20Root%20Certification%20Authority%202.crt" && \ +# curl -fsSL -o certs/cern-grid-ca-1.crt \ +# "https://ca.cern.ch/cafiles/certificates/CERN%20Grid%20Certification%20Authority(1).crt" +CCDB_CA_CERT_PATH= ALICETRAINT_NN_ARCHITECTURE_PATH=web/nn_architectures/proposed.json # If you do not want to install alien CA certs in ~/.globus/certificates # clone it yourself (https://github.com/alisw/alien-cas) and fill it with dirpath -JALIEN_CERT_CA_DIR= \ No newline at end of file +JALIEN_CERT_CA_DIR= diff --git a/Dockerfile b/Dockerfile index d59c6ef..0402df1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,20 @@ RUN chmod 0400 ./userkey.pem RUN git clone --depth=1 --branch master https://github.com/alisw/alien-cas.git /app/alien-cas RUN openssl rehash /app/alien-cas +# CERN CA bundle for CCDB TLS. alice-ccdb is issued by CERN Grid CA, which +# chains to CERN Root CA 2; server often omits the intermediate, so both needed. +# Soft-fail so offline/CI builds still succeed. +RUN mkdir -p /app/certs && \ + (curl -fsSL --retry 3 --retry-delay 2 \ + -o /app/certs/cern-root-ca-2.crt \ + "https://ca.cern.ch/cafiles/certificates/CERN%20Root%20Certification%20Authority%202.crt" \ + && curl -fsSL --retry 3 --retry-delay 2 \ + -o /app/certs/cern-grid-ca-1.crt \ + "https://ca.cern.ch/cafiles/certificates/CERN%20Grid%20Certification%20Authority(1).crt" \ + && echo "CERN CCDB CA bundle downloaded") \ + || (echo "WARNING: failed to download CERN CA bundle; CCDB will fall back to system roots" >&2; \ + rm -f /app/certs/cern-root-ca-2.crt /app/certs/cern-grid-ca-1.crt) + # --- runtime stage (same for both) --- FROM registry.access.redhat.com/ubi9 AS runtime @@ -53,11 +67,13 @@ ENV JALIEN_CERT_CA_DIR=/app/alien-cas COPY --from=builder /app/AliceTraINT ./ COPY --from=builder /app/usercert.pem ./usercert.pem COPY --from=builder /app/userkey.pem ./userkey.pem +COPY --from=builder /app/certs ./certs COPY --from=builder /app/.env ./ RUN chmod 0400 ./userkey.pem ENV GRID_CERT_PATH=./usercert.pem ENV GRID_KEY_PATH=./userkey.pem +ENV CCDB_CA_CERT_PATH=/app/certs # Docs and web templates are loaded at the execution # of Go binary so they can be copied here and css generated diff --git a/internal/ccdb/requests.go b/internal/ccdb/requests.go index 7cc66b3..4498c5a 100644 --- a/internal/ccdb/requests.go +++ b/internal/ccdb/requests.go @@ -13,9 +13,15 @@ import ( ) // Inspired by retrieveHeaders method of O2 CcdbApi -func doRemoteHeaderCall(url, uniqueAgentID string, timestamp int64) (map[string]string, error) { +func doRemoteHeaderCall(url, uniqueAgentID string, timestamp int64, tlsConfig *tls.Config) (map[string]string, error) { + transport := http.DefaultTransport.(*http.Transport).Clone() + if tlsConfig != nil { + transport.TLSClientConfig = tlsConfig + } + client := &http.Client{ - Timeout: 10 * time.Second, + Timeout: 10 * time.Second, + Transport: transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, @@ -30,12 +36,20 @@ func doRemoteHeaderCall(url, uniqueAgentID string, timestamp int64) (map[string] req.Header.Set("User-Agent", uniqueAgentID) resp, err := client.Do(req) - if err != nil && !isUnsupportedProtocol(err) { + if err != nil { return nil, fmt.Errorf("request failed: %w", err) } //nolint:errcheck defer resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("CCDB: run information not found") + } + + if resp.StatusCode < 200 || resp.StatusCode >= 400 { + return nil, fmt.Errorf("CCDB: unexpected status %d", resp.StatusCode) + } + headers := make(map[string]string) for key, values := range resp.Header { if len(values) == 1 { @@ -45,17 +59,9 @@ func doRemoteHeaderCall(url, uniqueAgentID string, timestamp int64) (map[string] } } - if resp.StatusCode == http.StatusNotFound { - headers = nil - } - return headers, nil } -func isUnsupportedProtocol(err error) bool { - return strings.Contains(err.Error(), "unsupported protocol") -} - func removeExtension(filename string) string { ext := filepath.Ext(filename) return strings.TrimSuffix(filename, ext) diff --git a/internal/ccdb/run_information.go b/internal/ccdb/run_information.go index 5cae3ed..55923cc 100644 --- a/internal/ccdb/run_information.go +++ b/internal/ccdb/run_information.go @@ -1,6 +1,7 @@ package ccdb import ( + "crypto/tls" "fmt" "strconv" ) @@ -16,32 +17,32 @@ const ( AGENT string = "AliceTraINT_Agent/1.0" ) -func GetRunInformation(baseURL string, runNumber uint64) (*RunInformation, error) { +func GetRunInformation(baseURL string, runNumber uint64, tlsConfig *tls.Config) (*RunInformation, error) { url := fmt.Sprintf("%s/%s/%d", baseURL, RCT_ENDPOINT, runNumber) - headers, err := doRemoteHeaderCall(url, AGENT, -1) + headers, err := doRemoteHeaderCall(url, AGENT, -1, tlsConfig) if err != nil { return nil, err } sorStr, sorOk := headerValue(headers, "Sor", "SOR") if !sorOk { - return nil, fmt.Errorf("CCDB: SOR not present for run %d (headers: %v)", runNumber, headers) + return nil, fmt.Errorf("CCDB: SOR not present for run %d", runNumber) } eorStr, eorOk := headerValue(headers, "Eor", "EOR") if !eorOk { - return nil, fmt.Errorf("CCDB: EOR not present for run %d (headers: %v)", runNumber, headers) + return nil, fmt.Errorf("CCDB: EOR not present for run %d", runNumber) } sor, err := parseUint64(sorStr) if err != nil { - return nil, err + return nil, fmt.Errorf("CCDB: invalid SOR for run %d: %w", runNumber, err) } eor, err := parseUint64(eorStr) if err != nil { - return nil, err + return nil, fmt.Errorf("CCDB: invalid EOR for run %d: %w", runNumber, err) } return &RunInformation{ diff --git a/internal/ccdb/upload_file.go b/internal/ccdb/upload_file.go index c9cb5a8..59761bd 100644 --- a/internal/ccdb/upload_file.go +++ b/internal/ccdb/upload_file.go @@ -5,16 +5,6 @@ import ( "io" ) -func UploadFile(uploadSubdirUrl string, cert *tls.Certificate, sor, eor uint64, filename string, file io.Reader) error { - ssl := &tls.Config{ - Certificates: []tls.Certificate{*cert}, - InsecureSkipVerify: true, - } - - err := uploadFile(filename, uploadSubdirUrl, file, sor, eor, ssl) - if err != nil { - return err - } - - return nil +func UploadFile(uploadSubdirUrl string, tlsConfig *tls.Config, sor, eor uint64, filename string, file io.Reader) error { + return uploadFile(filename, uploadSubdirUrl, file, sor, eor, tlsConfig) } diff --git a/internal/config/config.go b/internal/config/config.go index ba4e083..7affa22 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,6 +20,7 @@ type Config struct { MonalisaBaseURL string CCDBBaseURL string CCDBUploadSubdir string + CCDBCACertPath string CertPath string KeyPath string DataDirPath string @@ -65,6 +66,7 @@ func LoadConfig() *Config { MonalisaBaseURL: getEnv("MONALISA_URL", "https://alimonitor.cern.ch"), CCDBBaseURL: getEnv("CCDB_URL", "http://ccdb-test.cern.ch:8080"), CCDBUploadSubdir: getEnv("CCDB_UPLOAD_SUBDIR", "/Users/m/mmytkows"), + CCDBCACertPath: getEnv("CCDB_CA_CERT_PATH", ""), CertPath: getEnv("GRID_CERT_PATH", ""), KeyPath: getEnv("GRID_KEY_PATH", ""), DataDirPath: getEnv("ALICETRAINT_DATA_DIR_PATH", "data"), diff --git a/internal/gridtls/cert_file.go b/internal/gridtls/cert_file.go new file mode 100644 index 0000000..052d665 --- /dev/null +++ b/internal/gridtls/cert_file.go @@ -0,0 +1,109 @@ +package gridtls + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "path/filepath" + "strings" +) + +// LoadCertPoolWithOptionalFile returns the system cert pool with optional +// extra CA material appended. certPath may be a single PEM/DER file or a +// directory of .crt/.pem files. Empty path leaves the system pool unchanged; +// a non-nil error is a soft warning for the caller. +func LoadCertPoolWithOptionalFile(certPath string) (*x509.CertPool, error) { + pool, err := x509.SystemCertPool() + if err != nil || pool == nil { + pool = x509.NewCertPool() + if err != nil { + err = fmt.Errorf("system cert pool unavailable: %w", err) + } + } + + if certPath == "" { + return pool, err + } + + if appendErr := appendCertPath(pool, certPath); appendErr != nil { + if err != nil { + return pool, fmt.Errorf("%v; also failed to load %s: %w", err, certPath, appendErr) + } + return pool, fmt.Errorf("failed to load extra CA %s: %w", certPath, appendErr) + } + + return pool, nil +} + +func appendCertPath(pool *x509.CertPool, certPath string) error { + info, err := os.Stat(certPath) + if err != nil { + return err + } + if !info.IsDir() { + return appendCertFile(pool, certPath) + } + + entries, err := os.ReadDir(certPath) + if err != nil { + return err + } + + loaded := 0 + var firstErr error + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + lower := strings.ToLower(name) + if !strings.HasSuffix(lower, ".crt") && !strings.HasSuffix(lower, ".pem") && !strings.HasSuffix(lower, ".cer") { + continue + } + if fileErr := appendCertFile(pool, filepath.Join(certPath, name)); fileErr != nil { + if firstErr == nil { + firstErr = fmt.Errorf("%s: %w", name, fileErr) + } + continue + } + loaded++ + } + + if loaded == 0 { + if firstErr != nil { + return firstErr + } + return fmt.Errorf("no CA certificates found in directory") + } + return nil +} + +func appendCertFile(pool *x509.CertPool, certPath string) error { + data, err := os.ReadFile(certPath) + if err != nil { + return err + } + if len(data) == 0 { + return fmt.Errorf("file is empty") + } + + if pool.AppendCertsFromPEM(data) { + return nil + } + + // CERN Root CA 2 is published as DER (.crt). + cert, err := x509.ParseCertificate(data) + if err != nil { + block, _ := pem.Decode(data) + if block != nil { + cert, err = x509.ParseCertificate(block.Bytes) + } + if err != nil { + return fmt.Errorf("not valid PEM or DER certificate: %w", err) + } + } + + pool.AddCert(cert) + return nil +} diff --git a/internal/gridtls/cert_file_test.go b/internal/gridtls/cert_file_test.go new file mode 100644 index 0000000..9a52c57 --- /dev/null +++ b/internal/gridtls/cert_file_test.go @@ -0,0 +1,64 @@ +package gridtls + +import ( + "os" + "path/filepath" + "testing" +) + +func TestAppendCertFile_DER(t *testing.T) { + path := "/tmp/cern-root-ca-2.crt" + if _, err := os.Stat(path); err != nil { + t.Skip("CERN Root CA fixture missing at", path) + } + + pool, err := LoadCertPoolWithOptionalFile(path) + if err != nil { + t.Fatalf("LoadCertPoolWithOptionalFile: %v", err) + } + if pool == nil { + t.Fatal("expected non-nil pool") + } +} + +func TestLoadCertPoolWithOptionalFile_Missing(t *testing.T) { + pool, err := LoadCertPoolWithOptionalFile("/nonexistent/cern-root-ca-2.crt") + if pool == nil { + t.Fatal("expected fallback pool") + } + if err == nil { + t.Fatal("expected soft error for missing file") + } +} + +func TestLoadCertPoolWithOptionalFile_Directory(t *testing.T) { + rootPath := "/tmp/cern-root-ca-2.crt" + gridPath := "/tmp/cern-grid-ca-1.crt" + if _, err := os.Stat(rootPath); err != nil { + t.Skip("CERN Root CA fixture missing") + } + if _, err := os.Stat(gridPath); err != nil { + t.Skip("CERN Grid CA fixture missing") + } + + dir := t.TempDir() + mustCopy := func(src, name string) { + data, err := os.ReadFile(src) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, name), data, 0o644); err != nil { + t.Fatal(err) + } + } + mustCopy(rootPath, "cern-root-ca-2.crt") + mustCopy(gridPath, "cern-grid-ca-1.crt") + + pool, err := LoadCertPoolWithOptionalFile(dir) + if err != nil { + t.Fatalf("load dir: %v", err) + } + if pool == nil { + t.Fatal("expected non-nil pool") + } +} diff --git a/internal/gridtls/root_cas.go b/internal/gridtls/root_cas.go new file mode 100644 index 0000000..65fd4a3 --- /dev/null +++ b/internal/gridtls/root_cas.go @@ -0,0 +1,208 @@ +package gridtls + +import ( + "crypto/x509" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// LoadRootCAs builds a CertPool from well-known grid CA locations. +// If none are available, the system cert pool is used. +func LoadRootCAs(certDir string) (*x509.CertPool, error) { + // 0) If CERT dir is explicitly set, use it + if certDir != "" { + if pool, ok := loadCertPoolFromDir(certDir); ok { + return pool, nil + } + } + + homeDir, _ := os.UserHomeDir() + + // Try to populate a local CA bundle similar to `alien.py getCAcerts`. + if homeDir != "" { + _ = ensureLocalGridCAs(homeDir) // best-effort; ignore error and fall back to other locations + } + + // 1) Local getCAcerts default location: ~/.globus/certificates + if homeDir != "" { + localDir := filepath.Join(homeDir, ".globus", "certificates") + if pool, ok := loadCertPoolFromDir(localDir); ok { + return pool, nil + } + } + + // 2) Common grid CA locations (CVMFS and system-wide) + gridDirs := []string{ + "/cvmfs/alice.cern.ch/etc/grid-security/certificates", + "/Users/Shared/cvmfs/alice.cern.ch/etc/grid-security/certificates", + "/etc/grid-security/certificates", + } + for _, d := range gridDirs { + if pool, ok := loadCertPoolFromDir(d); ok { + return pool, nil + } + } + + // 3) Fallback to system roots. + sysPool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + return sysPool, nil +} + +func loadCertPoolFromDir(dir string) (*x509.CertPool, bool) { + info, err := os.Stat(dir) + if err != nil || !info.IsDir() { + return nil, false + } + + pool := x509.NewCertPool() + found := false + + err = filepath.WalkDir(dir, func(path string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if d.IsDir() { + return nil + } + if !strings.HasSuffix(d.Name(), ".pem") && !strings.HasSuffix(d.Name(), ".crt") { + return nil + } + + data, readErr := os.ReadFile(path) + if readErr != nil { + return nil + } + if pool.AppendCertsFromPEM(data) { + found = true + } + return nil + }) + + if err != nil || !found { + return nil, false + } + return pool, true +} + +// ensureLocalGridCAs roughly replicates `alien.py getCAcerts`: +// it clones the ALICE CA bundle from GitHub into ~/.globus/certificates if +// that directory doesn't already contain any PEM/CRT files. +func ensureLocalGridCAs(homeDir string) error { + baseDir := filepath.Join(homeDir, ".globus") + certDir := filepath.Join(baseDir, "certificates") + + // If we already have some CA files, do nothing. + if _, ok := loadCertPoolFromDir(certDir); ok { + return nil + } + + // Require git; if it's not available, just return silently. + if _, err := exec.LookPath("git"); err != nil { + return nil + } + + tempDir := filepath.Join(baseDir, "aliencas_temp") + + _ = os.RemoveAll(tempDir) + _ = os.RemoveAll(certDir) + + if err := os.MkdirAll(baseDir, 0o755); err != nil { + return err + } + + cloneCmd := exec.Command( + "git", "clone", + "--single-branch", "--branch", "master", "--depth=1", + "https://github.com/alisw/alien-cas.git", + tempDir, + ) + cloneCmd.Stdout = io.Discard + cloneCmd.Stderr = io.Discard + if err := cloneCmd.Run(); err != nil { + _ = os.RemoveAll(tempDir) + return err + } + + // Copy all files from tempDir into certDir, excluding any .git directory. + if err := copyTree(tempDir, certDir); err != nil { + _ = os.RemoveAll(tempDir) + return err + } + + _ = os.RemoveAll(tempDir) + + // Best-effort rehash, as in alien.py (not required for Go, but harmless). + if _, err := exec.LookPath("openssl"); err == nil { + _ = exec.Command("openssl", "rehash", certDir).Run() + } else if _, err := exec.LookPath("c_rehash"); err == nil { + _ = exec.Command("c_rehash", certDir).Run() + } + + return nil +} + +// copyTree recursively copies all files and subdirectories from src to dst. +// Existing files are overwritten. +func copyTree(src, dst string) error { + return filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + rel, relErr := filepath.Rel(src, path) + if relErr != nil { + return relErr + } + if rel == "." { + return nil + } + + target := filepath.Join(dst, rel) + + if d.IsDir() { + // Skip VCS metadata. + if d.Name() == ".git" { + return filepath.SkipDir + } + return os.MkdirAll(target, 0o755) + } + + return copyFile(path, target) + }) +} + +func copyFile(src, dst string) error { + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return err + } + in, err := os.Open(src) + if err != nil { + return err + } + defer func() { + if err := in.Close(); err != nil { + log.Printf("error closing file: %v\n", err) + } + }() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if err := out.Close(); err != nil { + log.Printf("error closing file: %v\n", err) + } + }() + + if _, err := io.Copy(out, in); err != nil { + return err + } + return out.Sync() +} diff --git a/internal/jalien/client.go b/internal/jalien/client.go index a0d7d9c..43b4525 100644 --- a/internal/jalien/client.go +++ b/internal/jalien/client.go @@ -3,20 +3,15 @@ package jalien import ( "context" "crypto/tls" - "crypto/x509" "encoding/json" "errors" "fmt" - "io" "log" "net/http" - "os" - "os/exec" - "path/filepath" "strconv" - "strings" "time" + "github.com/mytkom/AliceTraINT/internal/gridtls" "nhooyr.io/websocket" ) @@ -54,7 +49,7 @@ func NewClient(host, port, certPath, keyPath, certDir string, timeoutSeconds uin return nil, err } - rootCAs, err := loadRootCAs(certDir) + rootCAs, err := gridtls.LoadRootCAs(certDir) if err != nil { return nil, err } @@ -78,203 +73,6 @@ func NewClient(host, port, certPath, keyPath, certDir string, timeoutSeconds uin }, nil } -// loadRootCAs attempts to build a CertPool based on well-known grid CA -// locations. If none are available, the system cert pool is used. -func loadRootCAs(certDir string) (*x509.CertPool, error) { - // 0) If CERT dir is explicitly set, use it - if certDir != "" { - if pool, ok := loadCertPoolFromDir(certDir); ok { - return pool, nil - } - } - - homeDir, _ := os.UserHomeDir() - - // Try to populate a local CA bundle similar to `alien.py getCAcerts`. - if homeDir != "" { - _ = ensureLocalGridCAs(homeDir) // best-effort; ignore error and fall back to other locations - } - - // 1) Local getCAcerts default location: ~/.globus/certificates - if homeDir != "" { - localDir := filepath.Join(homeDir, ".globus", "certificates") - if pool, ok := loadCertPoolFromDir(localDir); ok { - return pool, nil - } - } - - // 2) Common grid CA locations (CVMFS and system-wide) - gridDirs := []string{ - "/cvmfs/alice.cern.ch/etc/grid-security/certificates", - "/Users/Shared/cvmfs/alice.cern.ch/etc/grid-security/certificates", - "/etc/grid-security/certificates", - } - for _, d := range gridDirs { - if pool, ok := loadCertPoolFromDir(d); ok { - return pool, nil - } - } - - // 3) Fallback to system roots. - sysPool, err := x509.SystemCertPool() - if err != nil { - return nil, err - } - return sysPool, nil -} - -func loadCertPoolFromDir(dir string) (*x509.CertPool, bool) { - info, err := os.Stat(dir) - if err != nil || !info.IsDir() { - return nil, false - } - - pool := x509.NewCertPool() - found := false - - err = filepath.WalkDir(dir, func(path string, d os.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - if d.IsDir() { - return nil - } - if !strings.HasSuffix(d.Name(), ".pem") && !strings.HasSuffix(d.Name(), ".crt") { - return nil - } - - data, readErr := os.ReadFile(path) - if readErr != nil { - return nil - } - if pool.AppendCertsFromPEM(data) { - found = true - } - return nil - }) - - if err != nil || !found { - return nil, false - } - return pool, true -} - -// ensureLocalGridCAs roughly replicates `alien.py getCAcerts`: -// it clones the ALICE CA bundle from GitHub into ~/.globus/certificates if -// that directory doesn't already contain any PEM/CRT files. -func ensureLocalGridCAs(homeDir string) error { - baseDir := filepath.Join(homeDir, ".globus") - certDir := filepath.Join(baseDir, "certificates") - - // If we already have some CA files, do nothing. - if _, ok := loadCertPoolFromDir(certDir); ok { - return nil - } - - // Require git; if it's not available, just return silently. - if _, err := exec.LookPath("git"); err != nil { - return nil - } - - tempDir := filepath.Join(baseDir, "aliencas_temp") - - _ = os.RemoveAll(tempDir) - _ = os.RemoveAll(certDir) - - if err := os.MkdirAll(baseDir, 0o755); err != nil { - return err - } - - cloneCmd := exec.Command( - "git", "clone", - "--single-branch", "--branch", "master", "--depth=1", - "https://github.com/alisw/alien-cas.git", - tempDir, - ) - cloneCmd.Stdout = io.Discard - cloneCmd.Stderr = io.Discard - if err := cloneCmd.Run(); err != nil { - _ = os.RemoveAll(tempDir) - return err - } - - // Copy all files from tempDir into certDir, excluding any .git directory. - if err := copyTree(tempDir, certDir); err != nil { - _ = os.RemoveAll(tempDir) - return err - } - - _ = os.RemoveAll(tempDir) - - // Best-effort rehash, as in alien.py (not required for Go, but harmless). - if _, err := exec.LookPath("openssl"); err == nil { - _ = exec.Command("openssl", "rehash", certDir).Run() - } else if _, err := exec.LookPath("c_rehash"); err == nil { - _ = exec.Command("c_rehash", certDir).Run() - } - - return nil -} - -// copyTree recursively copies all files and subdirectories from src to dst. -// Existing files are overwritten. -func copyTree(src, dst string) error { - return filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error { - if err != nil { - return err - } - rel, relErr := filepath.Rel(src, path) - if relErr != nil { - return relErr - } - if rel == "." { - return nil - } - - target := filepath.Join(dst, rel) - - if d.IsDir() { - // Skip VCS metadata. - if d.Name() == ".git" { - return filepath.SkipDir - } - return os.MkdirAll(target, 0o755) - } - - return copyFile(path, target) - }) -} - -func copyFile(src, dst string) error { - if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { - return err - } - in, err := os.Open(src) - if err != nil { - return err - } - defer func() { - if err := in.Close(); err != nil { - log.Printf("error closing file: %v\n", err) - } - }() - - out, err := os.Create(dst) - if err != nil { - return err - } - defer func() { - if err := out.Close(); err != nil { - log.Printf("error closing file: %v\n", err) - } - }() - - if _, err := io.Copy(out, in); err != nil { - return err - } - return out.Sync() -} - // send issues a single JAliEn command and returns the decoded response. func (c *Client) send(ctx context.Context, cmd string, options []string) (*jalienResponse, error) { if c == nil { diff --git a/internal/service/ccdb_service.go b/internal/service/ccdb_service.go index 9bc4fb4..4f68dd6 100644 --- a/internal/service/ccdb_service.go +++ b/internal/service/ccdb_service.go @@ -2,6 +2,7 @@ package service import ( "crypto/tls" + "crypto/x509" "fmt" "io" "log" @@ -9,6 +10,7 @@ import ( "github.com/mytkom/AliceTraINT/internal/applog" "github.com/mytkom/AliceTraINT/internal/ccdb" "github.com/mytkom/AliceTraINT/internal/environment" + "github.com/mytkom/AliceTraINT/internal/gridtls" "github.com/stretchr/testify/mock" ) @@ -21,6 +23,7 @@ type CCDBService struct { baseURL string uploadSubdir string cert tls.Certificate + rootCAs *x509.CertPool } func NewCCDBService(env *environment.Env) *CCDBService { @@ -29,16 +32,30 @@ func NewCCDBService(env *environment.Env) *CCDBService { log.Fatalf("cannot create CCDBService: %s", err.Error()) } + rootCAs, err := gridtls.LoadCertPoolWithOptionalFile(env.CCDBCACertPath) + if err != nil { + log.Printf("CCDB: using system roots only (%v)", err) + } + return &CCDBService{ baseURL: env.CCDBBaseURL, uploadSubdir: env.CCDBUploadSubdir, cert: cert, + rootCAs: rootCAs, + } +} + +func (s *CCDBService) tlsConfig() *tls.Config { + return &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{s.cert}, + RootCAs: s.rootCAs, } } func (s *CCDBService) GetRunInformation(runNumber uint64) (*ccdb.RunInformation, error) { return applog.ExternalCallResult("CCDB", "GetRunInformation", fmt.Sprintf("run=%d", runNumber), func() (*ccdb.RunInformation, error) { - return ccdb.GetRunInformation(s.baseURL, runNumber) + return ccdb.GetRunInformation(s.baseURL, runNumber, s.tlsConfig()) }) } @@ -46,7 +63,7 @@ func (s *CCDBService) UploadFile(sor, eor uint64, filename string, file io.Reade return applog.ExternalCall("CCDB", "UploadFile", fmt.Sprintf("file=%s sor=%d eor=%d", filename, sor, eor), func() error { return ccdb.UploadFile( fmt.Sprintf("%s/%s", s.baseURL, s.uploadSubdir), - &s.cert, + s.tlsConfig(), sor, eor, filename, diff --git a/internal/service/handler_errors.go b/internal/service/handler_errors.go index 0e9fc4f..cbc0159 100644 --- a/internal/service/handler_errors.go +++ b/internal/service/handler_errors.go @@ -1,9 +1,12 @@ package service import ( + "crypto/x509" "errors" "fmt" + "net" "os" + "strings" ) type ErrInternal struct { @@ -37,12 +40,59 @@ func mapCCDBError(op string, err error) error { if err == nil { return nil } - if os.IsTimeout(err) { + if isCCDBUnreachable(err) { return errCCDBUnreachable } return InternalError(op, err) } +func isCCDBUnreachable(err error) bool { + if err == nil { + return false + } + if os.IsTimeout(err) { + return true + } + + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return true + } + + var opErr *net.OpError + if errors.As(err, &opErr) { + return true + } + + var dnsErr *net.DNSError + if errors.As(err, &dnsErr) { + return true + } + + var unknownAuth *x509.UnknownAuthorityError + if errors.As(err, &unknownAuth) { + return true + } + + var certInvalid *x509.CertificateInvalidError + if errors.As(err, &certInvalid) { + return true + } + + var hostnameErr x509.HostnameError + if errors.As(err, &hostnameErr) { + return true + } + + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "certificate") || + strings.Contains(msg, "tls:") || + strings.Contains(msg, "x509:") || + strings.Contains(msg, "connection refused") || + strings.Contains(msg, "no such host") || + strings.Contains(msg, "network is unreachable") +} + func mapJAlienError(_ string, err error) error { if err == nil { return nil