certdataDiffCCADB/main.go (217 lines of code) (raw):

/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package main // import "github.com/mozilla/CCADB-Tools/utils" import ( "encoding/json" "flag" "fmt" "io" "log" "net/http" "os" "path" "sync" "github.com/mozilla/CCADB-Tools/certdataDiffCCADB/ccadb" "github.com/mozilla/CCADB-Tools/certdataDiffCCADB/certdata" "github.com/mozilla/CCADB-Tools/certdataDiffCCADB/utils" "github.com/throttled/throttled" "github.com/throttled/throttled/store/memstore" ) // Hard coded output filenames. const ( matched = "matched.json" unmatchedTrusted = "unmatchedTrusted.json" unmatchedUntrusted = "unmatchedUntrusted.json" ) var certdataURL string var ccadbURL string var certdataPath string var ccadbPath string var outDir string var matchedPath string var unmatchedTrustPath string var unmatchedUntrustedPath string var serverMode bool func init() { log.SetFlags(log.LstdFlags | log.Lshortfile) flag.StringVar(&certdataPath, "cd", "", "Path to certdata.txt") flag.StringVar(&certdataURL, "cdurl", certdata.URL, "URL to certdata.txt") flag.StringVar(&ccadbPath, "ccadb", "", "Path to CCADB report file.") flag.StringVar(&ccadbURL, "ccadburl", ccadb.URL, "URL to CCADB report file.") flag.StringVar(&outDir, "o", "", "Path to the output directory.") flag.BoolVar(&serverMode, "serve", false, "Start in server mode. While in server mode the "+ "/certdata endpoint is available which downloads a copy of certdata.txt from the default URL and returns a "+ "simplified JSON representation. This option requires that the PORT environment variable be set.") matchedPath = path.Join(outDir, matched) unmatchedTrustPath = path.Join(outDir, unmatchedTrusted) unmatchedUntrustedPath = path.Join(outDir, unmatchedUntrusted) } // Functions used for single run mode. // CCCADBReader constructs an io.ReaderCloser depending on the results // of parsing the CLI flags. The presence of a filepath will return an io.ReadCloser // with a concrete type of os.File. Otherwise, the io.ReadCloser is backed by // and http.Response which is pulling the data from the URL parsed in the CLI flags. func CCADBReader() io.ReadCloser { if ccadbPath != "" { log.Printf("Loading CCADB data from %s\n", ccadbPath) // get the stream from a file stream, err := os.Open(ccadbPath) if err != nil { log.Fatalf("Problem loading CCADB data from file %s\n", err) } return stream } else { log.Printf("Loading CCADB data from %s\n", ccadbURL) // get the stream from URL r, err := getFromURL(ccadbURL) if err != nil { log.Fatalf("Problem fetching CCADB data from URL %s\n", err) } return r } } // CertdataReader constructs an io.ReaderCloser depending on the results // of parsing the CLI flags. The presence of a filepath will return an io.ReadCloser // with a concrete type of os.File. Otherwise, the io.ReadCloser is backed by // and http.Response which is pulling the data from the URL parsed in the CLI flags. func CertdataReader() io.ReadCloser { if certdataPath != "" { log.Printf("Loading certdata.txt data from %s\n", certdataPath) // get the stream from a file stream, err := os.Open(certdataPath) if err != nil { log.Fatalf("Problem loading certdata.txt data from file %s\n", err) } return stream } else { log.Printf("Loading certdata.txt data from %s\n", certdataURL) r, err := getFromURL(certdataURL) // get the stream from URL if err != nil { log.Fatalf("Problem fetching certdata.txt data from URL %s\n", err) } return r } } func getFromURL(url string) (io.ReadCloser, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Add("X-Automated-Tool", `https://github.com/mozilla/CCADB-Tools utils"`) r, err := http.DefaultClient.Do(req) if err != nil { return nil, err } return r.Body, nil } func writeJSON(v interface{}, fname string, wg *sync.WaitGroup) { defer wg.Done() f, err := os.Create(fname) if err != nil { log.Fatal(err) } defer f.Close() j, err := json.MarshalIndent(v, "", " ") if err != nil { log.Fatal(err) return } if _, err := f.Write(j); err != nil { log.Fatal(err) } } func parse(src func() io.ReadCloser, parser func(io.Reader) ([]*utils.Entry, error), out chan<- []*utils.Entry) { r := src() defer r.Close() result, err := parser(r) if err != nil { log.Println(err) out <- nil return } out <- result } // Functions used for server mode. // SimpleEntry is a subset of the utils.Entry use to provide a simpler // view of the certdata file while in server mode. type SimpleEntry struct { PEM string `json:"PEM"` Fingerprint string `json:"sha256"` SerialNumber string `json:"serialNumber"` Issuer string `json:"issuer"` TrustWeb bool `json:"trustWeb"` TrustEmail bool `json:"trustEmail"` } // ListCertdata returns to the client a JSON array of SimpleEntry func ListCertdata(w http.ResponseWriter, req *http.Request) { defer func() { if err := recover(); err != nil { w.WriteHeader(http.StatusBadGateway) w.Write([]byte(fmt.Sprintf("%v\n", err))) } }() q := req.URL.Query() url := certdataURL if u, ok := q["url"]; ok && len(u) > 0 { url = u[0] } log.Printf("ListCertdata, IP: %v, certdata.txt URL: %v\n", req.RemoteAddr, url) stream, err := getFromURL(url) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(fmt.Sprintln(err.Error()))) log.Printf("ListCertdata, IP: %v, Error: %v\n", req.RemoteAddr, err) return } defer stream.Close() c, err := certdata.ParseToNormalizedForm(stream) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(fmt.Sprintln(err.Error()))) log.Printf("ListCertdata, IP: %v, Error: %v\n", req.RemoteAddr, err) return } resp := make([]SimpleEntry, len(c)) for i, e := range c { resp[i] = SimpleEntry{PEM: e.PEM, Fingerprint: e.Fingerprint, SerialNumber: e.SerialNumber, Issuer: e.DistinguishedName(), TrustWeb: e.TrustWeb, TrustEmail: e.TrustEmail} } if err := json.NewEncoder(w).Encode(resp); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(fmt.Sprintln(err.Error()))) log.Printf("ListCertdata, IP: %v, Error: %v\n", req.RemoteAddr, err) return } } // Runner function for starting the server. func serve() { // Setup rate limiting. store, err := memstore.New(65536) if err != nil { log.Fatal(err) } // 20 per minute, with a burst of 5. quota := throttled.RateQuota{MaxRate: throttled.PerMin(20), MaxBurst: 5} rateLimiter, err := throttled.NewGCRARateLimiter(store, quota) if err != nil { log.Fatal(err) } httpRateLimiter := throttled.HTTPRateLimiter{ RateLimiter: rateLimiter, VaryBy: &throttled.VaryBy{Path: true}, } rateLimitedHandler := httpRateLimiter.RateLimit(http.HandlerFunc(ListCertdata)) // Setup server and launch. http.Handle("/certdata", rateLimitedHandler) log.Println("Starting in server mode.") port := fmt.Sprintf(":%v", os.Getenv("PORT")) log.Printf("Listening on port %v\n", port) log.Fatal(http.ListenAndServe(port, nil)) } // Runner functions for either single run mode or server mode. func singleRun() { cdResult := make(chan []*utils.Entry) CCADBResult := make(chan []*utils.Entry) go parse(CertdataReader, certdata.ParseToNormalizedForm, cdResult) go parse(CCADBReader, ccadb.ParseToNormalizedForm, CCADBResult) cd := <-cdResult ccadb := <-CCADBResult if cd == nil || ccadb == nil { log.Fatal("One or more errors have occurred.") } matched, unmatchedT, unmatchedUT := utils.MapPairs(cd, ccadb) wg := new(sync.WaitGroup) wg.Add(3) go writeJSON(matched, matchedPath, wg) go writeJSON(unmatchedT, unmatchedTrustPath, wg) go writeJSON(unmatchedUT, unmatchedUntrustedPath, wg) wg.Wait() } func main() { flag.Parse() if serverMode { serve() } else { singleRun() } }