diff options
Diffstat (limited to 'bin-src')
-rwxr-xr-x | bin-src/cron-daily | 5 | ||||
-rwxr-xr-x | bin-src/crtsh-getcerts | 28 | ||||
-rw-r--r-- | bin-src/crtsh-pem2html.go | 150 | ||||
-rw-r--r-- | bin-src/diff-pem2html.go | 109 | ||||
-rw-r--r-- | bin-src/pem-diff.go | 140 | ||||
-rw-r--r-- | bin-src/tls-getcerts.go | 192 | ||||
-rw-r--r-- | bin-src/tls-pem2html.go | 160 | ||||
-rw-r--r-- | bin-src/util/date.go | 51 | ||||
-rw-r--r-- | bin-src/util/html.go | 14 |
9 files changed, 849 insertions, 0 deletions
diff --git a/bin-src/cron-daily b/bin-src/cron-daily new file mode 100755 index 0000000..7b71669 --- /dev/null +++ b/bin-src/cron-daily @@ -0,0 +1,5 @@ +#!/bin/sh +cd "$(dirname -- "$0")/.." +date > NET-crtsh +date > NET-tls +make diff --git a/bin-src/crtsh-getcerts b/bin-src/crtsh-getcerts new file mode 100755 index 0000000..0191e2e --- /dev/null +++ b/bin-src/crtsh-getcerts @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby +require 'nokogiri' +require 'open-uri' + +certs = {} +ARGV.each do |domain| + [ domain, "%.#{domain}" ].each do |pattern| + Nokogiri::XML(open("https://crt.sh/atom?identity=#{pattern}&exclude=expired")).css('feed > entry').each do |entry| + url = entry.css('id').first.text.split("#").first + + updated = entry.css('updated').first.text + + html = Nokogiri::HTML(entry.css('summary').first.text) + html.css('br').each{|br| br.replace("\n")} + pem = html.css('div').first.text + + lines = pem.split("\n") + lines.insert(1, "X-Crt-Sh-Url: #{url}", "X-Crt-Sh-Updated: #{updated}") + pem = lines.join("\n")+"\n" + + certs[url] = pem + end + end +end + +certs.each do |url, pem| + print pem +end diff --git a/bin-src/crtsh-pem2html.go b/bin-src/crtsh-pem2html.go new file mode 100644 index 0000000..109917c --- /dev/null +++ b/bin-src/crtsh-pem2html.go @@ -0,0 +1,150 @@ +package main + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "html/template" + "io/ioutil" + "os" + "sort" + "time" + + "./util" +) + +func handleErr(err error, str string, a ...interface{}) { + a = append([]interface{}{err}, a...) + if err != nil { + fmt.Fprintf(os.Stderr, str, a...) + os.Exit(1) + } +} + +func handleBool(ok bool, str string, a ...interface{}) { + if !ok { + fmt.Fprintf(os.Stderr, str, a...) + os.Exit(1) + } +} + +var tmpl = template.Must(template.New("pem2html"). + Funcs(template.FuncMap{ + "red": red, + "green": green, + "date": util.Date2HTML, + "datetime": util.DateTime2HTML, + "colorDatetime": util.DateTime2ColorHTML, + }).Parse(`<table class=sortable> + <caption> + <p>CT log (Updated {{.now | colorDatetime}})</p> + </caption> + <tr> + <th>Logged</th> + <th>NotBefore</th> + <th>NotAfter</th> + <th>Subject.CN</th> + <th>Issuer.O</th> + </tr> +{{range $cert := .certs}} + <tr> + <td style="background-color: {{$cert.Updated | green}}"><a target="_blank" href="{{$cert.Url}}">{{$cert.Updated | date}}</a></td> + <td style="background-color: {{$cert.X509.NotBefore | green}}"><a target="_blank" href="{{$cert.Url}}">{{$cert.X509.NotBefore | date}}</a></td> + <td style="background-color: {{$cert.X509.NotAfter | red }}"><a target="_blank" href="{{$cert.Url}}">{{$cert.X509.NotAfter | date}}</a></td> + <td><a target="_blank" href="{{$cert.Url}}">{{$cert.X509.Subject.CommonName}}</a></td> + <td><a target="_blank" href="{{$cert.Url}}">{{$cert.X509.Issuer.Organization}}</a></td> + </tr> +{{end}} +</table> +`)) + +func getNow() time.Time { + stat, err := os.Stdin.Stat() + if err == nil { + return stat.ModTime() + } else { + return time.Now() + } +} + +var now = getNow() + +func green(t time.Time) string { + max := byte(0xF3) + // When did we get the cert? + // - 30 days ago => 0 green + // - just now => max green + greenness := util.MapRange( + util.TimeRange{now.AddDate(0, 0, -30), now}, + util.ByteRange{0, max}, + t) + return fmt.Sprintf("#%02X%02X%02X", max-greenness, max, max-greenness) +} + +func red(t time.Time) string { + max := byte(0xF3) + // When with the cert expire? + // - now => max red + // - 30 days from now => 0 red + redness := util.MapRange( + util.TimeRange{now, now.AddDate(0, 0, 30)}, + util.ByteRange{max, 0}, + t) + return fmt.Sprintf("#%02X%02X%02X", max, max-redness, max-redness) +} + +type Cert struct { + Url string + Updated time.Time + X509 *x509.Certificate +} + +type Certs []Cert + +// Len is the number of elements in the collection. +func (l Certs) Len() int { + return len(l) +} + +// Less reports whether the element with +// index i should sort before the element with index j. +func (l Certs) Less(i, j int) bool { + return l[i].Updated.After(l[j].Updated) +} + +// Swap swaps the elements with indexes i and j. +func (l Certs) Swap(i, j int) { + tmp := l[i] + l[i] = l[j] + l[j] = tmp +} + +func main() { + data, err := ioutil.ReadAll(os.Stdin) + handleErr(err, "Error reading stdin: %v\n") + + var certs Certs + for len(data) > 0 { + var certPem *pem.Block + certPem, data = pem.Decode(data) + + var ok bool + var cert Cert + + cert.Url, ok = certPem.Headers["X-Crt-Sh-Url"] + handleBool(ok, "Did not get X-Crt-Sh-Url\n") + + str, ok := certPem.Headers["X-Crt-Sh-Updated"] + handleBool(ok, "Did not get X-Crt-Sh-Updated\n") + cert.Updated, err = time.Parse("2006-01-02T15:04:05Z", str) + handleErr(err, "Could not parse updated time") + + cert.X509, err = x509.ParseCertificate(certPem.Bytes) + handleErr(err, "Error parsing cert: %v\n") + + certs = append(certs, cert) + } + + sort.Sort(certs) + handleErr(tmpl.Execute(os.Stdout, map[string]interface{}{"certs": certs, "now": now}), "Could not execute template: %v\n") +} diff --git a/bin-src/diff-pem2html.go b/bin-src/diff-pem2html.go new file mode 100644 index 0000000..f3b25ff --- /dev/null +++ b/bin-src/diff-pem2html.go @@ -0,0 +1,109 @@ +package main + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "html/template" + "io/ioutil" + "os" + + "./util" +) + +func handleErr(err error, str string, a ...interface{}) { + a = append([]interface{}{err}, a...) + if err != nil { + fmt.Fprintf(os.Stderr, str, a...) + os.Exit(1) + } +} + +func handleBool(ok bool, str string, a ...interface{}) { + if !ok { + fmt.Fprintf(os.Stderr, str, a...) + os.Exit(1) + } +} + +var tmpl = template.Must(template.New("pem2html"). + Funcs(template.FuncMap{ + "htmlcell": util.HTMLCellEscapeString, + }).Parse(`<table class=diff> + <tr class="diff-del"><td colspan=4>--- tls.pem</td></tr> + <tr class="diff-add"><td colspan=4>+++ crtsh.pem</td></tr> + <tr class="diff-dat"><td colspan=4>@@ -1,{{.nTLS}} +1,{{.nCrtSh}} @@</td></tr> +{{range $cert := .certs}} + <tr class={{$cert.Class}}> + <td><a href="{{$cert.Url}}">{{$cert.Pfix | htmlcell}}</a></td> + <td><a href="{{$cert.Url}}">{{$cert.X509.Subject.CommonName | htmlcell}}</a></td> + <td><a href="{{$cert.Url}}">{{$cert.X509.NotBefore.Local.Format "2006-01-02 15:04:05"}}</a></td> + <td><a href="{{$cert.Url}}">{{$cert.X509.NotAfter.Local.Format "2006-01-02 15:04:05"}}</a></td> + </tr> +{{end}} +</table> +`)) + +type Cert struct { + Url string + action string + X509 *x509.Certificate +} + +func (cert Cert) Pfix() string { + return map[string]string{ + "add": "+", + "del": "-", + "ctx": " ", + }[cert.action] +} + +func (cert Cert) Class() string { + return "diff-" + cert.action +} + +func main() { + data, err := ioutil.ReadAll(os.Stdin) + handleErr(err, "Error reading stdin: %v\n") + + var certs []Cert + a := 0 + b := 0 + for len(data) > 0 { + var certPem *pem.Block + certPem, data = pem.Decode(data) + + var ok bool + var cert Cert + + cert.Url, ok = certPem.Headers["X-Crt-Sh-Url"] + handleBool(ok, "Did not get X-Crt-Sh-Url\n") + + cert.action, ok = certPem.Headers["X-Diff-Action"] + handleBool(ok, "Did not get X-Diff-Action\n") + switch cert.action { + case "add": + b++ + case "del": + a++ + case "ctx": + a++ + b++ + default: + handleBool(false, "Unknown X-Diff-Action: %q\n", cert.action) + } + + cert.X509, err = x509.ParseCertificate(certPem.Bytes) + if err != nil { + cert.X509 = new(x509.Certificate) + } + + certs = append(certs, cert) + } + + handleErr(tmpl.Execute(os.Stdout, map[string]interface{}{ + "certs": certs, + "nTLS": a, + "nCrtSh": b, + }), "Could not execute template: %v\n") +} diff --git a/bin-src/pem-diff.go b/bin-src/pem-diff.go new file mode 100644 index 0000000..da27a62 --- /dev/null +++ b/bin-src/pem-diff.go @@ -0,0 +1,140 @@ +package main + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "net/url" + "os" + "sort" + "strings" +) + +func handleErr(err error, str string, a ...interface{}) { + a = append([]interface{}{err}, a...) + if err != nil { + fmt.Fprintf(os.Stderr, str, a...) + os.Exit(1) + } +} + +type Cert struct { + Url string + X509 *x509.Certificate +} + +func (cert Cert) WriteTo(w io.Writer, action string) error { + block := pem.Block{ + Type: "CERTIFICATE", + Headers: map[string]string{ + "X-Crt-Sh-Url": cert.Url, + "X-Diff-Action": action, + }, + Bytes: cert.X509.Raw, + } + return pem.Encode(w, &block) +} + +func readTLS(filename string) (map[string]Cert, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + data, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + + ret := make(map[string]Cert) + for len(data) > 0 { + var certPem *pem.Block + certPem, data = pem.Decode(data) + certX509, err := x509.ParseCertificate(certPem.Bytes) + if err != nil { + url, err2 := url.Parse(certPem.Headers["X-Socket"]) + if err2 != nil { + fmt.Fprintf(os.Stderr, "Could not get cert or even parse URL:\ncert: %v\nurl: %v\n", err, err2) + os.Exit(1) + } + ret[strings.Split(url.Host, ":")[0]] = Cert{ + X509: new(x509.Certificate), + } + } else { + ret[certX509.Subject.CommonName] = Cert{ + Url: fmt.Sprintf("https://crt.sh/?serial=%036x", certX509.SerialNumber), + X509: certX509, + } + } + } + return ret, nil +} + +func readCrtSh(filename string, hosts []string) (map[string]Cert, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + data, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + + ret := make(map[string]Cert) + for len(data) > 0 { + var certPem *pem.Block + certPem, data = pem.Decode(data) + certX509, err := x509.ParseCertificate(certPem.Bytes) + if err != nil { + return nil, err + } + for _, host := range hosts { + if certX509.VerifyHostname(host) == nil { + if old, haveold := ret[host]; !haveold || certX509.NotBefore.After(old.X509.NotBefore) { + ret[host] = Cert{ + Url: certPem.Headers["X-Crt-Sh-Url"], + X509: certX509, + } + } + } + } + } + return ret, nil +} + +func keys(m map[string]Cert) []string { + ret := make([]string, len(m)) + i := 0 + for k := range m { + ret[i] = k + i++ + } + sort.Strings(ret) + return ret +} + +func main() { + if len(os.Args) != 3 { + fmt.Fprintf(os.Stderr, "Usage: %s TLS-file crt.sh-file\n", os.Args[0]) + } + certsTLS, err := readTLS(os.Args[1]) + handleErr(err, "Could load TLS file: %v\n") + hostsTLS := keys(certsTLS) + certsCrtSh, err := readCrtSh(os.Args[2], hostsTLS) + handleErr(err, "Could load crt.sh file: %v\n") + + for _, host := range hostsTLS { + certTLS := certsTLS[host] + certCrtSh, haveCrtSh := certsCrtSh[host] + + if !haveCrtSh { + handleErr(certTLS.WriteTo(os.Stdout, "del"), "Could not encode PEM: %v\n") + } else if !certTLS.X509.Equal(certCrtSh.X509) { + handleErr(certTLS.WriteTo(os.Stdout, "del"), "Could not encode PEM: %v\n") + handleErr(certCrtSh.WriteTo(os.Stdout, "add"), "Could not encode PEM: %v\n") + } else { + handleErr(certCrtSh.WriteTo(os.Stdout, "ctx"), "Could not encode PEM: %v\n") + } + } +} diff --git a/bin-src/tls-getcerts.go b/bin-src/tls-getcerts.go new file mode 100644 index 0000000..34e25e5 --- /dev/null +++ b/bin-src/tls-getcerts.go @@ -0,0 +1,192 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "encoding/pem" + "encoding/xml" + "fmt" + "io" + "net" + "net/textproto" + "net/url" + "os" + "strings" + "time" +) + +type xmppStreamsFeatures struct { + XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` +} + +type xmppTlsProceed struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"` +} + +func xmppStartTLS(connRaw net.Conn, host string) error { + decoder := xml.NewDecoder(connRaw) + + // send <stream> start + _, err := fmt.Fprintf(connRaw, "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='%s' version='1.0'>", host) + if err != nil { + return err + } + // read <stream> start + for { + t, err := decoder.Token() + if err != nil || t == nil { + return err + } + if se, ok := t.(xml.StartElement); ok { + if se.Name.Local != "stream" { + return xml.UnmarshalError(fmt.Sprintf("expected element of type <%s> but have <%s>", "stream", se.Name.Local)) + } + break + } + } + // read <features> + var features xmppStreamsFeatures + err = decoder.DecodeElement(&features, nil) + if err != nil { + return err + } + // send <starttls> + _, err = io.WriteString(connRaw, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>") + if err != nil { + return err + } + // read <proceed> + var proceed xmppTlsProceed + err = decoder.DecodeElement(&proceed, nil) + if err != nil { + return err + } + return nil +} + +// smtpCmd is a convenience function that sends a command, and reads +// (but discards) the response +func smtpCmd(tp *textproto.Conn, expectCode int, format string, args ...interface{}) error { + id, err := tp.Cmd(format, args...) + if err != nil { + return err + } + tp.StartResponse(id) + defer tp.EndResponse(id) + _, _, err = tp.ReadResponse(expectCode) + return err +} + +func smtpStartTLS(connRaw net.Conn, host string) error { + tp := textproto.NewConn(connRaw) + + // let the server introduce itself + _, _, err := tp.ReadResponse(220) + if err != nil { + return err + } + // introduce ourself + localhost, err := os.Hostname() + if err != nil { + localhost = "localhost" + } + err = smtpCmd(tp, 250, "EHLO %s", localhost) + if err != nil { + err := smtpCmd(tp, 250, "HELO %s", localhost) + if err != nil { + return err + } + } + // starttls + err = smtpCmd(tp, 220, "STARTTLS") + if err != nil { + return err + } + return nil +} + +func getcert(socket string) (*x509.Certificate, error) { + u, err := url.Parse(socket) + if err != nil { + return nil, err + } + host, _, err := net.SplitHostPort(u.Host) + if err != nil { + return nil, err + } + + connRaw, err := net.Dial(u.Scheme, u.Host) + if err != nil { + return nil, err + } + err = connRaw.SetDeadline(time.Now().Add(5 * time.Second)) + if err != nil { + return nil, err + } + + switch u.Path { + case "", "/": + // do nothing + case "/xmpp": + err = xmppStartTLS(connRaw, host) + if err != nil { + return nil, err + } + case "/smtp": + err = smtpStartTLS(connRaw, host) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("Unknown negotiation path: %q", u.Path) + } + + connTLS := tls.Client(connRaw, &tls.Config{InsecureSkipVerify: true}) + defer connTLS.Close() + err = connTLS.Handshake() + if err != nil { + return nil, err + } + + cstate := connTLS.ConnectionState() + + opts := x509.VerifyOptions{ + DNSName: host, + Intermediates: x509.NewCertPool(), + } + for _, cert := range cstate.PeerCertificates[1:] { + opts.Intermediates.AddCert(cert) + } + + cert := cstate.PeerCertificates[0] + _, err = cert.Verify(opts) + return cert, err +} + +func split(socket string) (net, addr string) { + ary := strings.SplitN(socket, ":", 2) + if len(ary) == 1 { + return "tcp", ary[0] + } + return ary[0], ary[1] +} + +func main() { + for _, socket := range os.Args[1:] { + fmt.Fprintf(os.Stderr, "Getting %q... ", socket) + block := pem.Block{ + Type: "CERTIFICATE", + Headers: map[string]string{"X-Socket": socket}, + Bytes: nil, + } + cert, err := getcert(socket) + if cert != nil { + block.Bytes = cert.Raw + } + if err != nil { + block.Headers["X-Error"] = err.Error() + } + pem.Encode(os.Stdout, &block) + fmt.Fprintln(os.Stderr, "[done]") + } +} diff --git a/bin-src/tls-pem2html.go b/bin-src/tls-pem2html.go new file mode 100644 index 0000000..bc14f9a --- /dev/null +++ b/bin-src/tls-pem2html.go @@ -0,0 +1,160 @@ +package main + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "html/template" + "io/ioutil" + "os" + "sort" + "time" + + "./util" +) + +func handleErr(err error, str string, a ...interface{}) { + a = append([]interface{}{err}, a...) + if err != nil { + fmt.Fprintf(os.Stderr, str, a...) + os.Exit(1) + } +} + +func handleBool(ok bool, str string, a ...interface{}) { + if !ok { + fmt.Fprintf(os.Stderr, str, a...) + os.Exit(1) + } +} + +var tmpl = template.Must(template.New("pem2html"). + Funcs(template.FuncMap{ + "red": red, + "green": green, + "date": util.Date2HTML, + "datetime": util.DateTime2HTML, + "colorDatetime": util.DateTime2ColorHTML, + "htmlcell": util.HTMLCellEscapeString, + }).Parse(`<table class=sortable> + <caption> + <p>Live Certs (Updated {{.now | colorDatetime}})</p> + </caption> + <tr> + <th>NotBefore</th> + <th>NotAfter</th> + <th>Subject.CN</th> + <th>Socket</th> + </tr> +{{range $cert := .certs}} + <tr class="{{$cert.Class}}"> + <td style="background-color: {{$cert.X509.NotBefore | green}}"><a href="{{$cert.Url}}" title="{{$cert.Error}}">{{$cert.X509.NotBefore | date}}</a></td> + <td style="background-color: {{$cert.X509.NotAfter | red }}"><a href="{{$cert.Url}}" title="{{$cert.Error}}">{{$cert.X509.NotAfter | date}}</a></td> + <td><a href="{{$cert.Url}}" title="{{$cert.Error}}">{{$cert.X509.Subject.CommonName | htmlcell}}</a></td> + <td><a href="{{$cert.Url}}" title="{{$cert.Error}}">{{$cert.Socket | htmlcell}}</a></td> + </tr> +{{end}} +</table> +`)) + +func getNow() time.Time { + stat, err := os.Stdin.Stat() + if err == nil { + return stat.ModTime() + } else { + return time.Now() + } +} + +var now = getNow() + +func green(t time.Time) string { + max := byte(0xF3) + // When did we get the cert? + // - 30 days ago => 0 green + // - just now => max green + greenness := util.MapRange( + util.TimeRange{now.AddDate(0, 0, -30), now}, + util.ByteRange{0, max}, + t) + return fmt.Sprintf("#%02X%02X%02X", max-greenness, max, max-greenness) +} + +func red(t time.Time) string { + max := byte(0xF3) + // When with the cert expire? + // - now => max red + // - 30 days from now => 0 red + redness := util.MapRange( + util.TimeRange{now, now.AddDate(0, 0, 30)}, + util.ByteRange{max, 0}, + t) + return fmt.Sprintf("#%02X%02X%02X", max, max-redness, max-redness) +} + +type Cert struct { + Socket string + Error string + X509 *x509.Certificate +} + +func (cert Cert) Url() string { + return fmt.Sprintf("https://crt.sh/?serial=%036x", cert.X509.SerialNumber) +} + +func (cert Cert) Class() string { + if cert.Error == "" { + return "" + } else { + return "invalid" + } +} + +type Certs []Cert + +// Len is the number of elements in the collection. +func (l Certs) Len() int { + return len(l) +} + +// Less reports whether the element with +// index i should sort before the element with index j. +func (l Certs) Less(i, j int) bool { + return l[i].X509.NotAfter.After(l[j].X509.NotAfter) +} + +// Swap swaps the elements with indexes i and j. +func (l Certs) Swap(i, j int) { + tmp := l[i] + l[i] = l[j] + l[j] = tmp +} + +func main() { + data, err := ioutil.ReadAll(os.Stdin) + handleErr(err, "Error reading stdin: %v\n") + + var certs Certs + for len(data) > 0 { + var certPem *pem.Block + certPem, data = pem.Decode(data) + + var ok bool + var cert Cert + + cert.Socket, ok = certPem.Headers["X-Socket"] + handleBool(ok, "Did not get X-Socket\n") + + cert.Error, ok = certPem.Headers["X-Error"] + + cert.X509, err = x509.ParseCertificate(certPem.Bytes) + if err != nil { + cert.X509 = new(x509.Certificate) + } + + certs = append(certs, cert) + } + + sort.Sort(certs) + handleErr(tmpl.Execute(os.Stdout, map[string]interface{}{"certs": certs, "now": now}), "Could not execute template: %v\n") +} diff --git a/bin-src/util/date.go b/bin-src/util/date.go new file mode 100644 index 0000000..3b5c457 --- /dev/null +++ b/bin-src/util/date.go @@ -0,0 +1,51 @@ +package util + +import ( + "html/template" + "time" +) + +func Date2HTML(t time.Time) template.HTML { + return template.HTML(t.Local().Format("<time datetime=\"2006-01-02 15:04:05\" title=\"2006-01-02 15:04:05\">2006-01-02 <span class=time>15:04:05</span></time>")) +} + +func DateTime2HTML(t time.Time) template.HTML { + return template.HTML(t.Local().Format("<time datetime=\"2006-01-02 15:04:05\">2006-01-02 15:04:05</time>")) +} + +func DateTime2ColorHTML(t time.Time) template.HTML { + return template.HTML(t.Local().Format("<time class=daily datetime=\"2006-01-02 15:04:05\">2006-01-02 15:04:05</time>")) +} + +type TimeRange struct { + A, B time.Time +} + +func (tr TimeRange) ToPct(point time.Time) float64 { + dur_ab := tr.B.Sub(tr.A) + dur_ap := point.Sub(tr.A) + return float64(dur_ap) / float64(dur_ab) +} + +type ByteRange struct { + A, B byte +} + +func (br ByteRange) FromPct(pct float64) byte { + ab := int16(br.B) - int16(br.A) + ap := int16(pct * float64(ab)) + return byte(int16(br.A) + ap) +} + +func PctCap(pct float64) float64 { + if pct < 0 { + pct = 0 + } else if pct > 1 { + pct = 1 + } + return pct +} + +func MapRange(tr TimeRange, br ByteRange, t time.Time) byte { + return br.FromPct(PctCap(tr.ToPct(t))) +} diff --git a/bin-src/util/html.go b/bin-src/util/html.go new file mode 100644 index 0000000..af2ce60 --- /dev/null +++ b/bin-src/util/html.go @@ -0,0 +1,14 @@ +package util + +import ( + "html/template" + "strings" +) + +func HTMLCellEscapeString(s string) template.HTML { + html := template.HTMLEscapeString(s) + if strings.TrimSpace(html) == "" { + html = " " + } + return template.HTML(html) +} |