"
}
ubuntu-push-0.2+14.04.20140411/whoopsie/identifier/identifier_test.go 0000644 0000153 0177776 00000002247 12322032412 025727 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package identifier
import (
. "launchpad.net/gocheck"
"testing"
)
// hook up gocheck
func Test(t *testing.T) { TestingT(t) }
type IdentifierSuite struct{}
var _ = Suite(&IdentifierSuite{})
// TestGenerate checks that Generate() does not fail, and returns a
// 128-byte string.
func (s *IdentifierSuite) TestGenerate(c *C) {
id := New()
c.Check(id.Generate(), Equals, nil)
c.Check(id.String(), HasLen, 128)
}
// TestIdentifierInterface checks that Identifier implements Id.
func (s *IdentifierSuite) TestIdentifierInterface(c *C) {
_ = []Id{New()}
}
ubuntu-push-0.2+14.04.20140411/external/ 0000755 0000153 0177776 00000000000 12322032700 020055 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/external/murmur3/ 0000755 0000153 0177776 00000000000 12322032700 021467 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/external/murmur3/murmur128.go 0000644 0000153 0177776 00000006756 12322032412 023616 0 ustar pbuser nogroup 0000000 0000000 package murmur3
import (
//"encoding/binary"
"hash"
"unsafe"
)
const (
c1_128 = 0x87c37b91114253d5
c2_128 = 0x4cf5ad432745937f
)
// Make sure interfaces are correctly implemented.
var (
_ hash.Hash = new(digest128)
_ Hash128 = new(digest128)
_ bmixer = new(digest128)
)
// Hack: the standard api doesn't define any Hash128 interface.
type Hash128 interface {
hash.Hash
Sum128() (uint64, uint64)
}
// digest128 represents a partial evaluation of a 128 bites hash.
type digest128 struct {
digest
h1 uint64 // Unfinalized running hash part 1.
h2 uint64 // Unfinalized running hash part 2.
}
func New128() Hash128 {
d := new(digest128)
d.bmixer = d
d.Reset()
return d
}
func (d *digest128) Size() int { return 16 }
func (d *digest128) reset() { d.h1, d.h2 = 1, 1 }
func (d *digest128) Sum(b []byte) []byte {
h1, h2 := d.h1, d.h2
return append(b,
byte(h1>>56), byte(h1>>48), byte(h1>>40), byte(h1>>32),
byte(h1>>24), byte(h1>>16), byte(h1>>8), byte(h1),
byte(h2>>56), byte(h2>>48), byte(h2>>40), byte(h2>>32),
byte(h2>>24), byte(h2>>16), byte(h2>>8), byte(h2),
)
}
func (d *digest128) bmix(p []byte) (tail []byte) {
h1, h2 := d.h1, d.h2
nblocks := len(p) / 16
for i := 0; i < nblocks; i++ {
t := (*[2]uint64)(unsafe.Pointer(&p[i*16]))
k1, k2 := t[0], t[1]
k1 *= c1_128
k1 = (k1 << 31) | (k1 >> 33) // rotl64(k1, 31)
k1 *= c2_128
h1 ^= k1
h1 = (h1 << 27) | (h1 >> 37) // rotl64(h1, 27)
h1 += h2
h1 = h1*5 + 0x52dce729
k2 *= c2_128
k2 = (k2 << 33) | (k2 >> 31) // rotl64(k2, 33)
k2 *= c1_128
h2 ^= k2
h2 = (h2 << 31) | (h2 >> 33) // rotl64(h2, 31)
h2 += h1
h2 = h2*5 + 0x38495ab5
}
d.h1, d.h2 = h1, h2
return p[nblocks*d.Size():]
}
func (d *digest128) Sum128() (h1, h2 uint64) {
h1, h2 = d.h1, d.h2
var k1, k2 uint64
switch len(d.tail) & 15 {
case 15:
k2 ^= uint64(d.tail[14]) << 48
fallthrough
case 14:
k2 ^= uint64(d.tail[13]) << 40
fallthrough
case 13:
k2 ^= uint64(d.tail[12]) << 32
fallthrough
case 12:
k2 ^= uint64(d.tail[11]) << 24
fallthrough
case 11:
k2 ^= uint64(d.tail[10]) << 16
fallthrough
case 10:
k2 ^= uint64(d.tail[9]) << 8
fallthrough
case 9:
k2 ^= uint64(d.tail[8]) << 0
k2 *= c2_128
k2 = (k2 << 33) | (k2 >> 31) // rotl64(k2, 33)
k2 *= c1_128
h2 ^= k2
fallthrough
case 8:
k1 ^= uint64(d.tail[7]) << 56
fallthrough
case 7:
k1 ^= uint64(d.tail[6]) << 48
fallthrough
case 6:
k1 ^= uint64(d.tail[5]) << 40
fallthrough
case 5:
k1 ^= uint64(d.tail[4]) << 32
fallthrough
case 4:
k1 ^= uint64(d.tail[3]) << 24
fallthrough
case 3:
k1 ^= uint64(d.tail[2]) << 16
fallthrough
case 2:
k1 ^= uint64(d.tail[1]) << 8
fallthrough
case 1:
k1 ^= uint64(d.tail[0]) << 0
k1 *= c1_128
k1 = (k1 << 31) | (k1 >> 33) // rotl64(k1, 31)
k1 *= c2_128
h1 ^= k1
}
h1 ^= uint64(d.clen)
h2 ^= uint64(d.clen)
h1 += h2
h2 += h1
h1 = fmix64(h1)
h2 = fmix64(h2)
h1 += h2
h2 += h1
return h1, h2
}
func fmix64(k uint64) uint64 {
k ^= k >> 33
k *= 0xff51afd7ed558ccd
k ^= k >> 33
k *= 0xc4ceb9fe1a85ec53
k ^= k >> 33
return k
}
/*
func rotl64(x uint64, r byte) uint64 {
return (x << r) | (x >> (64 - r))
}
*/
// Sum128 returns the MurmurHash3 sum of data. It is equivalent to the
// following sequence (without the extra burden and the extra allocation):
// hasher := New128()
// hasher.Write(data)
// return hasher.Sum128()
func Sum128(data []byte) (h1 uint64, h2 uint64) {
d := &digest128{h1: 1, h2: 1}
d.tail = d.bmix(data)
d.clen = len(data)
return d.Sum128()
}
ubuntu-push-0.2+14.04.20140411/external/murmur3/murmur64.go 0000644 0000153 0177776 00000001706 12322032412 023523 0 ustar pbuser nogroup 0000000 0000000 package murmur3
import (
"hash"
)
// Make sure interfaces are correctly implemented.
var (
_ hash.Hash = new(digest64)
_ hash.Hash64 = new(digest64)
_ bmixer = new(digest64)
)
// digest64 is half a digest128.
type digest64 digest128
func New64() hash.Hash64 {
d := (*digest64)(New128().(*digest128))
return d
}
func (d *digest64) Sum(b []byte) []byte {
h1 := d.h1
return append(b,
byte(h1>>56), byte(h1>>48), byte(h1>>40), byte(h1>>32),
byte(h1>>24), byte(h1>>16), byte(h1>>8), byte(h1))
}
func (d *digest64) Sum64() uint64 {
h1, _ := (*digest128)(d).Sum128()
return h1
}
// Sum64 returns the MurmurHash3 sum of data. It is equivalent to the
// following sequence (without the extra burden and the extra allocation):
// hasher := New64()
// hasher.Write(data)
// return hasher.Sum64()
func Sum64(data []byte) uint64 {
d := &digest128{h1: 1, h2: 1}
d.tail = d.bmix(data)
d.clen = len(data)
h1, _ := d.Sum128()
return h1
}
ubuntu-push-0.2+14.04.20140411/external/murmur3/murmur.go 0000644 0000153 0177776 00000003030 12322032412 023341 0 ustar pbuser nogroup 0000000 0000000 // Copyright 2013, Sébastien Paolacci. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Native (and fast) implementation of Austin Appleby's MurmurHash3.
Package murmur3 implements Austin Appleby's non-cryptographic MurmurHash3.
Reference implementation:
http://code.google.com/p/smhasher/wiki/MurmurHash3
History, characteristics and (legacy) perfs:
https://sites.google.com/site/murmurhash/
https://sites.google.com/site/murmurhash/statistics
*/
package murmur3
type bmixer interface {
bmix(p []byte) (tail []byte)
Size() (n int)
reset()
}
type digest struct {
clen int // Digested input cumulative length.
tail []byte // 0 to Size()-1 bytes view of `buf'.
buf [16]byte // Expected (but not required) to be Size() large.
bmixer
}
func (d *digest) BlockSize() int { return 1 }
func (d *digest) Write(p []byte) (n int, err error) {
n = len(p)
d.clen += n
if len(d.tail) > 0 {
// Stick back pending bytes.
nfree := d.Size() - len(d.tail) // nfree ∈ [1, d.Size()-1].
if nfree < len(p) {
// One full block can be formed.
block := append(d.tail, p[:nfree]...)
p = p[nfree:]
_ = d.bmix(block) // No tail.
} else {
// Tail's buf is large enough to prevent reallocs.
p = append(d.tail, p...)
}
}
d.tail = d.bmix(p)
// Keep own copy of the 0 to Size()-1 pending bytes.
nn := copy(d.buf[:], d.tail)
d.tail = d.buf[:nn]
return n, nil
}
func (d *digest) Reset() {
d.clen = 0
d.tail = nil
d.bmixer.reset()
}
ubuntu-push-0.2+14.04.20140411/external/murmur3/murmur32.go 0000644 0000153 0177776 00000005350 12322032412 023515 0 ustar pbuser nogroup 0000000 0000000 package murmur3
// http://code.google.com/p/guava-libraries/source/browse/guava/src/com/google/common/hash/Murmur3_32HashFunction.java
import (
"hash"
"unsafe"
)
// Make sure interfaces are correctly implemented.
var (
_ hash.Hash = new(digest32)
_ hash.Hash32 = new(digest32)
)
const (
c1_32 uint32 = 0xcc9e2d51
c2_32 uint32 = 0x1b873593
)
// digest32 represents a partial evaluation of a 32 bites hash.
type digest32 struct {
digest
h1 uint32 // Unfinalized running hash.
}
func New32() hash.Hash32 {
d := new(digest32)
d.bmixer = d
d.Reset()
return d
}
func (d *digest32) Size() int { return 4 }
func (d *digest32) reset() { d.h1 = 1 }
func (d *digest32) Sum(b []byte) []byte {
h := d.h1
return append(b, byte(h>>24), byte(h>>16), byte(h>>8), byte(h))
}
// Digest as many blocks as possible.
func (d *digest32) bmix(p []byte) (tail []byte) {
h1 := d.h1
nblocks := len(p) / 4
for i := 0; i < nblocks; i++ {
k1 := *(*uint32)(unsafe.Pointer(&p[i*4]))
k1 *= c1_32
k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15)
k1 *= c2_32
h1 ^= k1
h1 = (h1 << 13) | (h1 >> 19) // rotl32(h1, 13)
h1 = h1*5 + 0xe6546b64
}
d.h1 = h1
return p[nblocks*d.Size():]
}
func (d *digest32) Sum32() (h1 uint32) {
h1 = d.h1
var k1 uint32
switch len(d.tail) & 3 {
case 3:
k1 ^= uint32(d.tail[2]) << 16
fallthrough
case 2:
k1 ^= uint32(d.tail[1]) << 8
fallthrough
case 1:
k1 ^= uint32(d.tail[0])
k1 *= c1_32
k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15)
k1 *= c2_32
h1 ^= k1
}
h1 ^= uint32(d.clen)
h1 ^= h1 >> 16
h1 *= 0x85ebca6b
h1 ^= h1 >> 13
h1 *= 0xc2b2ae35
h1 ^= h1 >> 16
return h1
}
/*
func rotl32(x uint32, r byte) uint32 {
return (x << r) | (x >> (32 - r))
}
*/
// Sum32 returns the MurmurHash3 sum of data. It is equivalent to the
// following sequence (without the extra burden and the extra allocation):
// hasher := New32()
// hasher.Write(data)
// return hasher.Sum32()
func Sum32(data []byte) uint32 {
var h1 uint32 = 1
nblocks := len(data) / 4
var p uintptr
if len(data) > 0 {
p = uintptr(unsafe.Pointer(&data[0]))
}
p1 := p + uintptr(4*nblocks)
for ; p < p1; p += 4 {
k1 := *(*uint32)(unsafe.Pointer(p))
k1 *= c1_32
k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15)
k1 *= c2_32
h1 ^= k1
h1 = (h1 << 13) | (h1 >> 19) // rotl32(h1, 13)
h1 = h1*5 + 0xe6546b64
}
tail := data[nblocks*4:]
var k1 uint32
switch len(tail) & 3 {
case 3:
k1 ^= uint32(tail[2]) << 16
fallthrough
case 2:
k1 ^= uint32(tail[1]) << 8
fallthrough
case 1:
k1 ^= uint32(tail[0])
k1 *= c1_32
k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15)
k1 *= c2_32
h1 ^= k1
}
h1 ^= uint32(len(data))
h1 ^= h1 >> 16
h1 *= 0x85ebca6b
h1 ^= h1 >> 13
h1 *= 0xc2b2ae35
h1 ^= h1 >> 16
return h1
}
ubuntu-push-0.2+14.04.20140411/external/murmur3/README.md 0000644 0000153 0177776 00000004352 12322032412 022752 0 ustar pbuser nogroup 0000000 0000000 murmur3
=======
Native Go implementation of Austin Appleby's third MurmurHash revision (aka
MurmurHash3).
Reference algorithm has been slightly hacked as to support the streaming mode
required by Go's standard [Hash interface](http://golang.org/pkg/hash/#Hash).
Benchmarks
----------
Go tip as of 2013-03-11 (i.e almost go1.1), core i7 @ 3.4 Ghz. All runs
include hasher instanciation and sequence finalization.
Benchmark32_1 200000000 8.4 ns/op 119.39 MB/s
Benchmark32_2 200000000 9.5 ns/op 211.69 MB/s
Benchmark32_4 500000000 7.9 ns/op 506.24 MB/s
Benchmark32_8 200000000 9.4 ns/op 853.40 MB/s
Benchmark32_16 100000000 12.1 ns/op 1324.19 MB/s
Benchmark32_32 100000000 18.2 ns/op 1760.81 MB/s
Benchmark32_64 50000000 31.2 ns/op 2051.59 MB/s
Benchmark32_128 50000000 58.7 ns/op 2180.34 MB/s
Benchmark32_256 20000000 116.0 ns/op 2194.85 MB/s
Benchmark32_512 10000000 227.0 ns/op 2247.43 MB/s
Benchmark32_1024 5000000 449.0 ns/op 2276.88 MB/s
Benchmark32_2048 2000000 894.0 ns/op 2289.87 MB/s
Benchmark32_4096 1000000 1792.0 ns/op 2284.64 MB/s
Benchmark32_8192 500000 3559.0 ns/op 2301.33 MB/s
Benchmark128_1 50000000 33.2 ns/op 30.15 MB/s
Benchmark128_2 50000000 33.3 ns/op 59.99 MB/s
Benchmark128_4 50000000 35.4 ns/op 112.88 MB/s
Benchmark128_8 50000000 36.6 ns/op 218.30 MB/s
Benchmark128_16 50000000 35.5 ns/op 450.86 MB/s
Benchmark128_32 50000000 35.3 ns/op 905.84 MB/s
Benchmark128_64 50000000 44.3 ns/op 1443.76 MB/s
Benchmark128_128 50000000 58.2 ns/op 2201.02 MB/s
Benchmark128_256 20000000 85.3 ns/op 2999.88 MB/s
Benchmark128_512 10000000 142.0 ns/op 3592.97 MB/s
Benchmark128_1024 10000000 258.0 ns/op 3963.74 MB/s
Benchmark128_2048 5000000 494.0 ns/op 4144.65 MB/s
Benchmark128_4096 2000000 955.0 ns/op 4285.80 MB/s
Benchmark128_8192 1000000 1884.0 ns/op 4347.12 MB/s
ubuntu-push-0.2+14.04.20140411/external/murmur3/murmur_test.go 0000644 0000153 0177776 00000011067 12322032412 024411 0 ustar pbuser nogroup 0000000 0000000 package murmur3
import (
"hash"
"testing"
)
var data = []struct {
h32 uint32
h64_1 uint64
h64_2 uint64
s string
}{
{0x514e28b7, 0x4610abe56eff5cb5, 0x51622daa78f83583, ""},
{0xbb4abcad, 0xa78ddff5adae8d10, 0x128900ef20900135, "hello"},
{0x6f5cb2e9, 0x8b95f808840725c6, 0x1597ed5422bd493b, "hello, world"},
{0xf50e1f30, 0x2a929de9c8f97b2f, 0x56a41d99af43a2db, "19 Jan 2038 at 3:14:07 AM"},
{0x846f6a36, 0xfb3325171f9744da, 0xaaf8b92a5f722952, "The quick brown fox jumps over the lazy dog."},
}
func TestRef(t *testing.T) {
for _, elem := range data {
var h32 hash.Hash32 = New32()
h32.Write([]byte(elem.s))
if v := h32.Sum32(); v != elem.h32 {
t.Errorf("'%s': 0x%x (want 0x%x)", elem.s, v, elem.h32)
}
if v := Sum32([]byte(elem.s)); v != elem.h32 {
t.Errorf("'%s': 0x%x (want 0x%x)", elem.s, v, elem.h32)
}
var h64 hash.Hash64 = New64()
h64.Write([]byte(elem.s))
if v := h64.Sum64(); v != elem.h64_1 {
t.Errorf("'%s': 0x%x (want 0x%x)", elem.s, v, elem.h64_1)
}
var h128 Hash128 = New128()
h128.Write([]byte(elem.s))
if v1, v2 := h128.Sum128(); v1 != elem.h64_1 || v2 != elem.h64_2 {
t.Errorf("'%s': 0x%x-0x%x (want 0x%x-0x%x)", elem.s, v1, v2, elem.h64_1, elem.h64_2)
}
if v1, v2 := Sum128([]byte(elem.s)); v1 != elem.h64_1 || v2 != elem.h64_2 {
t.Errorf("'%s': 0x%x-0x%x (want 0x%x-0x%x)", elem.s, v1, v2, elem.h64_1, elem.h64_2)
}
}
}
func TestIncremental(t *testing.T) {
for _, elem := range data {
h32 := New32()
h128 := New128()
for i, j, k := 0, 0, len(elem.s); i < k; i = j {
j = 2*i + 3
if j > k {
j = k
}
s := elem.s[i:j]
print(s + "|")
h32.Write([]byte(s))
h128.Write([]byte(s))
}
println()
if v := h32.Sum32(); v != elem.h32 {
t.Errorf("'%s': 0x%x (want 0x%x)", elem.s, v, elem.h32)
}
if v1, v2 := h128.Sum128(); v1 != elem.h64_1 || v2 != elem.h64_2 {
t.Errorf("'%s': 0x%x-0x%x (want 0x%x-0x%x)", elem.s, v1, v2, elem.h64_1, elem.h64_2)
}
}
}
//---
func bench32(b *testing.B, length int) {
buf := make([]byte, length)
b.SetBytes(int64(length))
b.ResetTimer()
for i := 0; i < b.N; i++ {
Sum32(buf)
}
}
func Benchmark32_1(b *testing.B) {
bench32(b, 1)
}
func Benchmark32_2(b *testing.B) {
bench32(b, 2)
}
func Benchmark32_4(b *testing.B) {
bench32(b, 4)
}
func Benchmark32_8(b *testing.B) {
bench32(b, 8)
}
func Benchmark32_16(b *testing.B) {
bench32(b, 16)
}
func Benchmark32_32(b *testing.B) {
bench32(b, 32)
}
func Benchmark32_64(b *testing.B) {
bench32(b, 64)
}
func Benchmark32_128(b *testing.B) {
bench32(b, 128)
}
func Benchmark32_256(b *testing.B) {
bench32(b, 256)
}
func Benchmark32_512(b *testing.B) {
bench32(b, 512)
}
func Benchmark32_1024(b *testing.B) {
bench32(b, 1024)
}
func Benchmark32_2048(b *testing.B) {
bench32(b, 2048)
}
func Benchmark32_4096(b *testing.B) {
bench32(b, 4096)
}
func Benchmark32_8192(b *testing.B) {
bench32(b, 8192)
}
//---
func benchPartial32(b *testing.B, length int) {
buf := make([]byte, length)
b.SetBytes(int64(length))
start := (32 / 8) / 2
chunks := 7
k := length / chunks
tail := (length - start) % k
b.ResetTimer()
for i := 0; i < b.N; i++ {
hasher := New32()
hasher.Write(buf[0:start])
for j := start; j+k <= length; j += k {
hasher.Write(buf[j : j+k])
}
hasher.Write(buf[length-tail:])
hasher.Sum32()
}
}
func BenchmarkPartial32_8(b *testing.B) {
benchPartial32(b, 8)
}
func BenchmarkPartial32_16(b *testing.B) {
benchPartial32(b, 16)
}
func BenchmarkPartial32_32(b *testing.B) {
benchPartial32(b, 32)
}
func BenchmarkPartial32_64(b *testing.B) {
benchPartial32(b, 64)
}
func BenchmarkPartial32_128(b *testing.B) {
benchPartial32(b, 128)
}
//---
func bench128(b *testing.B, length int) {
buf := make([]byte, length)
b.SetBytes(int64(length))
b.ResetTimer()
for i := 0; i < b.N; i++ {
Sum128(buf)
}
}
func Benchmark128_1(b *testing.B) {
bench128(b, 1)
}
func Benchmark128_2(b *testing.B) {
bench128(b, 2)
}
func Benchmark128_4(b *testing.B) {
bench128(b, 4)
}
func Benchmark128_8(b *testing.B) {
bench128(b, 8)
}
func Benchmark128_16(b *testing.B) {
bench128(b, 16)
}
func Benchmark128_32(b *testing.B) {
bench128(b, 32)
}
func Benchmark128_64(b *testing.B) {
bench128(b, 64)
}
func Benchmark128_128(b *testing.B) {
bench128(b, 128)
}
func Benchmark128_256(b *testing.B) {
bench128(b, 256)
}
func Benchmark128_512(b *testing.B) {
bench128(b, 512)
}
func Benchmark128_1024(b *testing.B) {
bench128(b, 1024)
}
func Benchmark128_2048(b *testing.B) {
bench128(b, 2048)
}
func Benchmark128_4096(b *testing.B) {
bench128(b, 4096)
}
func Benchmark128_8192(b *testing.B) {
bench128(b, 8192)
}
//---
ubuntu-push-0.2+14.04.20140411/external/murmur3/LICENSE 0000644 0000153 0177776 00000002730 12322032412 022476 0 ustar pbuser nogroup 0000000 0000000 Copyright 2013, Sébastien Paolacci.
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 the library 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 LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ubuntu-push-0.2+14.04.20140411/external/README 0000644 0000153 0177776 00000000204 12322032412 020731 0 ustar pbuser nogroup 0000000 0000000 Directly included vendorized small packages.
* murmor3 comes from import at lp:~ubuntu-push-hackers/ubuntu-push/murmur at revno 10
ubuntu-push-0.2+14.04.20140411/dependencies.tsv 0000644 0000153 0177776 00000000511 12322032412 021414 0 ustar pbuser nogroup 0000000 0000000 code.google.com/p/gosqlite hg 74691fb6f83716190870cde1b658538dd4b18eb0 15
launchpad.net/go-dbus/v1 bzr james@jamesh.id.au-20140206110213-pbzcr6ucaz3rqmnw 125
launchpad.net/go-xdg/v0 bzr john.lenton@canonical.com-20140208094800-gubd5md7cro3mtxa 10
launchpad.net/gocheck bzr gustavo@niemeyer.net-20140127131816-zshobk1qqme626xw 86
ubuntu-push-0.2+14.04.20140411/ubuntu-push-client.go 0000644 0000153 0177776 00000002233 12322032412 022335 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package main
import (
"log"
"launchpad.net/go-xdg/v0"
"launchpad.net/ubuntu-push/client"
)
func main() {
cfgFname, err := xdg.Config.Find("ubuntu-push-client/config.json")
if err != nil {
log.Fatalf("unable to find a configuration file: %v", err)
}
lvlFname, err := xdg.Data.Ensure("ubuntu-push-client/levels.db")
if err != nil {
log.Fatalf("unable to open the levels database: %v", err)
}
cli := client.NewPushClient(cfgFname, lvlFname)
err = cli.Start()
if err != nil {
log.Fatalf("unable to start: %v", err)
}
cli.Loop()
}
ubuntu-push-0.2+14.04.20140411/tarmac_tests.sh 0000755 0000153 0177776 00000000170 12322032412 021261 0 ustar pbuser nogroup 0000000 0000000 #!/bin/bash
# For running tests in Jenkins by Tarmac
set -e
make bootstrap
make check
make check-race
make check-format
ubuntu-push-0.2+14.04.20140411/config/ 0000755 0000153 0177776 00000000000 12322032700 017500 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/config/config.go 0000644 0000153 0177776 00000014653 12322032412 021305 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package config has helpers to parse and use JSON based configuration.
package config
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"path/filepath"
"reflect"
"strings"
"time"
)
func checkDestConfig(name string, destConfig interface{}) (reflect.Value, error) {
destValue := reflect.ValueOf(destConfig)
if destValue.Kind() != reflect.Ptr || destValue.Elem().Kind() != reflect.Struct {
return reflect.Value{}, fmt.Errorf("%s not *struct", name)
}
return destValue, nil
}
type destField struct {
fld reflect.StructField
dest interface{}
}
func (f destField) configName() string {
fld := f.fld
configName := strings.Split(fld.Tag.Get("json"), ",")[0]
if configName == "" {
configName = strings.ToLower(fld.Name[:1]) + fld.Name[1:]
}
return configName
}
func traverseStruct(destStruct reflect.Value) <-chan destField {
ch := make(chan destField)
var traverse func(reflect.Value, chan<- destField)
traverse = func(destStruct reflect.Value, ch chan<- destField) {
structType := destStruct.Type()
n := structType.NumField()
for i := 0; i < n; i++ {
fld := structType.Field(i)
val := destStruct.Field(i)
if fld.PkgPath != "" { // unexported
continue
}
if fld.Anonymous {
traverse(val, ch)
continue
}
ch <- destField{
fld: fld,
dest: val.Addr().Interface(),
}
}
}
go func() {
traverse(destStruct, ch)
close(ch)
}()
return ch
}
func fillDestConfig(destValue reflect.Value, p map[string]json.RawMessage) error {
destStruct := destValue.Elem()
for destField := range traverseStruct(destStruct) {
configName := destField.configName()
raw, found := p[configName]
if !found { // assume all fields are mandatory for now
return fmt.Errorf("missing %s", configName)
}
dest := destField.dest
err := json.Unmarshal([]byte(raw), dest)
if err != nil {
return fmt.Errorf("%s: %v", configName, err)
}
}
return nil
}
// ReadConfig reads a JSON configuration into destConfig which should
// be a pointer to a structure. It does some more configuration
// specific error checking than plain JSON decoding, and mentions
// fields in errors. Configuration fields in the JSON object are
// expected to start with lower case.
func ReadConfig(r io.Reader, destConfig interface{}) error {
destValue, err := checkDestConfig("destConfig", destConfig)
if err != nil {
return err
}
// do the parsing in two phases for better error handling
var p1 map[string]json.RawMessage
err = json.NewDecoder(r).Decode(&p1)
if err != nil {
return err
}
return fillDestConfig(destValue, p1)
}
// ConfigTimeDuration can hold a time.Duration in a configuration struct,
// that is parsed from a string as supported by time.ParseDuration.
type ConfigTimeDuration struct {
time.Duration
}
func (ctd *ConfigTimeDuration) UnmarshalJSON(b []byte) error {
var enc string
var v time.Duration
err := json.Unmarshal(b, &enc)
if err != nil {
return err
}
v, err = time.ParseDuration(enc)
if err != nil {
return err
}
*ctd = ConfigTimeDuration{v}
return nil
}
// TimeDuration returns the time.Duration held in ctd.
func (ctd ConfigTimeDuration) TimeDuration() time.Duration {
return ctd.Duration
}
// ConfigHostPort can hold a host:port string in a configuration struct.
type ConfigHostPort string
func (chp *ConfigHostPort) UnmarshalJSON(b []byte) error {
var enc string
err := json.Unmarshal(b, &enc)
if err != nil {
return err
}
_, _, err = net.SplitHostPort(enc)
if err != nil {
return err
}
*chp = ConfigHostPort(enc)
return nil
}
// HostPort returns the host:port string held in chp.
func (chp ConfigHostPort) HostPort() string {
return string(chp)
}
// ConfigQueueSize can hold a queue size in a configuration struct.
type ConfigQueueSize uint
func (cqs *ConfigQueueSize) UnmarshalJSON(b []byte) error {
var enc uint
err := json.Unmarshal(b, &enc)
if err != nil {
return err
}
if enc == 0 {
return errors.New("queue size should be > 0")
}
*cqs = ConfigQueueSize(enc)
return nil
}
// QueueSize returns the queue size held in cqs.
func (cqs ConfigQueueSize) QueueSize() uint {
return uint(cqs)
}
// LoadFile reads a file possibly relative to a base dir.
func LoadFile(p, baseDir string) ([]byte, error) {
if p == "" {
return nil, nil
}
if !filepath.IsAbs(p) {
p = filepath.Join(baseDir, p)
}
return ioutil.ReadFile(p)
}
// ReadFiles reads configuration from a set of files. Uses ReadConfig internally.
func ReadFiles(destConfig interface{}, cfgFpaths ...string) error {
destValue, err := checkDestConfig("destConfig", destConfig)
if err != nil {
return err
}
// do the parsing in two phases for better error handling
var p1 map[string]json.RawMessage
readOne := false
for _, cfgPath := range cfgFpaths {
if _, err := os.Stat(cfgPath); err == nil {
r, err := os.Open(cfgPath)
if err != nil {
return err
}
defer r.Close()
err = json.NewDecoder(r).Decode(&p1)
if err != nil {
return err
}
readOne = true
}
}
if !readOne {
return fmt.Errorf("no config to read")
}
return fillDestConfig(destValue, p1)
}
// CompareConfigs compares the two given configuration structures. It returns a list of differing fields or nil if the config contents are the same.
func CompareConfig(config1, config2 interface{}) ([]string, error) {
v1, err := checkDestConfig("config1", config1)
if err != nil {
return nil, err
}
v2, err := checkDestConfig("config2", config2)
if err != nil {
return nil, err
}
if v1.Type() != v2.Type() {
return nil, errors.New("config1 and config2 don't have the same type")
}
fields1 := traverseStruct(v1.Elem())
fields2 := traverseStruct(v2.Elem())
diff := make([]string, 0)
for {
d1 := <-fields1
d2 := <-fields2
if d1.dest == nil {
break
}
if !reflect.DeepEqual(d1.dest, d2.dest) {
diff = append(diff, d1.configName())
}
}
if len(diff) != 0 {
return diff, nil
}
return nil, nil
}
ubuntu-push-0.2+14.04.20140411/config/config_test.go 0000644 0000153 0177776 00000014246 12322032412 022342 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package config
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"time"
. "launchpad.net/gocheck"
)
func TestConfig(t *testing.T) { TestingT(t) }
type configSuite struct{}
var _ = Suite(&configSuite{})
type testConfig1 struct {
A int
B string
C []string `json:"c_list"`
}
func (s *configSuite) TestReadConfig(c *C) {
buf := bytes.NewBufferString(`{"a": 1, "b": "foo", "c_list": ["c", "d", "e"]}`)
var cfg testConfig1
err := ReadConfig(buf, &cfg)
c.Check(err, IsNil)
c.Check(cfg, DeepEquals, testConfig1{A: 1, B: "foo", C: []string{"c", "d", "e"}})
}
func checkError(c *C, config string, dest interface{}, expectedError string) {
buf := bytes.NewBufferString(config)
err := ReadConfig(buf, dest)
c.Check(err, ErrorMatches, expectedError)
}
func (s *configSuite) TestReadConfigErrors(c *C) {
var cfg testConfig1
checkError(c, "", cfg, `destConfig not \*struct`)
var i int
checkError(c, "", &i, `destConfig not \*struct`)
checkError(c, "", &cfg, `EOF`)
checkError(c, `{"a": "1"}`, &cfg, `a: .*type int`)
checkError(c, `{"b": "1"}`, &cfg, `missing a`)
checkError(c, `{"A": "1"}`, &cfg, `missing a`)
checkError(c, `{"a": 1, "b": "foo"}`, &cfg, `missing c_list`)
}
type testTimeDurationConfig struct {
D ConfigTimeDuration
}
func (s *configSuite) TestReadConfigTimeDuration(c *C) {
buf := bytes.NewBufferString(`{"d": "2s"}`)
var cfg testTimeDurationConfig
err := ReadConfig(buf, &cfg)
c.Assert(err, IsNil)
c.Check(cfg.D.TimeDuration(), Equals, 2*time.Second)
}
func (s *configSuite) TestReadConfigTimeDurationErrors(c *C) {
var cfg testTimeDurationConfig
checkError(c, `{"d": 1}`, &cfg, "d:.*type string")
checkError(c, `{"d": "2"}`, &cfg, "d:.*missing unit.*")
}
type testHostPortConfig struct {
H ConfigHostPort
}
func (s *configSuite) TestReadConfigHostPort(c *C) {
buf := bytes.NewBufferString(`{"h": "127.0.0.1:9999"}`)
var cfg testHostPortConfig
err := ReadConfig(buf, &cfg)
c.Assert(err, IsNil)
c.Check(cfg.H.HostPort(), Equals, "127.0.0.1:9999")
}
func (s *configSuite) TestReadConfigHostPortErrors(c *C) {
var cfg testHostPortConfig
checkError(c, `{"h": 1}`, &cfg, "h:.*type string")
checkError(c, `{"h": ""}`, &cfg, "h: missing port in address")
}
type testQueueSizeConfig struct {
QS ConfigQueueSize
}
func (s *configSuite) TestReadConfigQueueSize(c *C) {
buf := bytes.NewBufferString(`{"qS": 1}`)
var cfg testQueueSizeConfig
err := ReadConfig(buf, &cfg)
c.Assert(err, IsNil)
c.Check(cfg.QS.QueueSize(), Equals, uint(1))
}
func (s *configSuite) TestReadConfigQueueSizeErrors(c *C) {
var cfg testQueueSizeConfig
checkError(c, `{"qS": "x"}`, &cfg, "qS: .*type uint")
checkError(c, `{"qS": 0}`, &cfg, "qS: queue size should be > 0")
}
func (s *configSuite) TestLoadFile(c *C) {
tmpDir := c.MkDir()
d, err := LoadFile("", tmpDir)
c.Check(err, IsNil)
c.Check(d, IsNil)
fullPath := filepath.Join(tmpDir, "example.file")
err = ioutil.WriteFile(fullPath, []byte("Example"), os.ModePerm)
c.Assert(err, IsNil)
d, err = LoadFile("example.file", tmpDir)
c.Check(err, IsNil)
c.Check(string(d), Equals, "Example")
d, err = LoadFile(fullPath, tmpDir)
c.Check(err, IsNil)
c.Check(string(d), Equals, "Example")
}
func (s *configSuite) TestReadFiles(c *C) {
tmpDir := c.MkDir()
cfg1Path := filepath.Join(tmpDir, "cfg1.json")
err := ioutil.WriteFile(cfg1Path, []byte(`{"a": 42}`), os.ModePerm)
c.Assert(err, IsNil)
cfg2Path := filepath.Join(tmpDir, "cfg2.json")
err = ioutil.WriteFile(cfg2Path, []byte(`{"b": "x", "c_list": ["y", "z"]}`), os.ModePerm)
c.Assert(err, IsNil)
var cfg testConfig1
err = ReadFiles(&cfg, cfg1Path, cfg2Path)
c.Assert(err, IsNil)
c.Check(cfg.A, Equals, 42)
c.Check(cfg.B, Equals, "x")
c.Check(cfg.C, DeepEquals, []string{"y", "z"})
}
func (s *configSuite) TestReadFilesErrors(c *C) {
var cfg testConfig1
err := ReadFiles(1)
c.Check(err, ErrorMatches, `destConfig not \*struct`)
err = ReadFiles(&cfg, "non-existent")
c.Check(err, ErrorMatches, "no config to read")
err = ReadFiles(&cfg, "/root")
c.Check(err, ErrorMatches, ".*permission denied")
tmpDir := c.MkDir()
err = ReadFiles(&cfg, tmpDir)
c.Check(err, ErrorMatches, ".*is a directory")
brokenCfgPath := filepath.Join(tmpDir, "b.json")
err = ioutil.WriteFile(brokenCfgPath, []byte(`{"a"-`), os.ModePerm)
c.Assert(err, IsNil)
err = ReadFiles(&cfg, brokenCfgPath)
c.Check(err, NotNil)
}
type B struct {
BFld int
}
type A struct {
AFld int
B
private int
}
func (s *configSuite) TestTraverseStruct(c *C) {
var a A
var i = 1
for destField := range traverseStruct(reflect.ValueOf(&a).Elem()) {
*(destField.dest.(*int)) = i
i++
}
c.Check(a, DeepEquals, A{1, B{2}, 0})
}
type testConfig2 struct {
A int
B string
C []string `json:"c_list"`
D ConfigTimeDuration
}
func (s *configSuite) TestCompareConfig(c *C) {
var cfg1 = testConfig2{
A: 1,
B: "xyz",
C: []string{"a", "b"},
D: ConfigTimeDuration{200 * time.Millisecond},
}
var cfg2 = testConfig2{
A: 1,
B: "xyz",
C: []string{"a", "b"},
D: ConfigTimeDuration{200 * time.Millisecond},
}
_, err := CompareConfig(cfg1, &cfg2)
c.Check(err, ErrorMatches, `config1 not \*struct`)
_, err = CompareConfig(&cfg1, cfg2)
c.Check(err, ErrorMatches, `config2 not \*struct`)
_, err = CompareConfig(&cfg1, &testConfig1{})
c.Check(err, ErrorMatches, `config1 and config2 don't have the same type`)
res, err := CompareConfig(&cfg1, &cfg2)
c.Assert(err, IsNil)
c.Check(res, IsNil)
cfg1.B = "zyx"
cfg2.C = []string{"a", "B"}
cfg2.D = ConfigTimeDuration{205 * time.Millisecond}
res, err = CompareConfig(&cfg1, &cfg2)
c.Assert(err, IsNil)
c.Check(res, DeepEquals, []string{"b", "c_list", "d"})
}
ubuntu-push-0.2+14.04.20140411/sampleconfigs/ 0000755 0000153 0177776 00000000000 12322032700 021065 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/sampleconfigs/dev.json 0000644 0000153 0177776 00000000631 12322032421 022536 0 ustar pbuser nogroup 0000000 0000000 {
"exchange_timeout": "5s",
"ping_interval": "10s",
"broker_queue_size": 10000,
"session_queue_size": 10,
"addr": "127.0.0.1:9090",
"key_pem_file": "../server/acceptance/ssl/testing.key",
"cert_pem_file": "../server/acceptance/ssl/testing.cert",
"http_addr": "127.0.0.1:8080",
"http_read_timeout": "5s",
"http_write_timeout": "5s",
"delivery_domain": "localhost"
}
ubuntu-push-0.2+14.04.20140411/server/ 0000755 0000153 0177776 00000000000 12322032700 017541 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/runner_devices.go 0000644 0000153 0177776 00000007074 12322032412 023113 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package server
import (
"fmt"
"net"
"syscall"
"time"
"launchpad.net/ubuntu-push/config"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/server/listener"
)
// A DevicesParsedConfig holds and can be used to parse the device server config.
type DevicesParsedConfig struct {
// session configuration
ParsedPingInterval config.ConfigTimeDuration `json:"ping_interval"`
ParsedExchangeTimeout config.ConfigTimeDuration `json:"exchange_timeout"`
// broker configuration
ParsedSessionQueueSize config.ConfigQueueSize `json:"session_queue_size"`
ParsedBrokerQueueSize config.ConfigQueueSize `json:"broker_queue_size"`
// device listener configuration
ParsedAddr config.ConfigHostPort `json:"addr"`
ParsedKeyPEMFile string `json:"key_pem_file"`
ParsedCertPEMFile string `json:"cert_pem_file"`
// private post-processed config
certPEMBlock []byte
keyPEMBlock []byte
}
func (cfg *DevicesParsedConfig) FinishLoad(baseDir string) error {
keyPEMBlock, err := config.LoadFile(cfg.ParsedKeyPEMFile, baseDir)
if err != nil {
return fmt.Errorf("reading key_pem_file: %v", err)
}
certPEMBlock, err := config.LoadFile(cfg.ParsedCertPEMFile, baseDir)
if err != nil {
return fmt.Errorf("reading cert_pem_file: %v", err)
}
cfg.keyPEMBlock = keyPEMBlock
cfg.certPEMBlock = certPEMBlock
return nil
}
func (cfg *DevicesParsedConfig) PingInterval() time.Duration {
return cfg.ParsedPingInterval.TimeDuration()
}
func (cfg *DevicesParsedConfig) ExchangeTimeout() time.Duration {
return cfg.ParsedExchangeTimeout.TimeDuration()
}
func (cfg *DevicesParsedConfig) SessionQueueSize() uint {
return cfg.ParsedSessionQueueSize.QueueSize()
}
func (cfg *DevicesParsedConfig) BrokerQueueSize() uint {
return cfg.ParsedBrokerQueueSize.QueueSize()
}
func (cfg *DevicesParsedConfig) Addr() string {
return cfg.ParsedAddr.HostPort()
}
func (cfg *DevicesParsedConfig) KeyPEMBlock() []byte {
return cfg.keyPEMBlock
}
func (cfg *DevicesParsedConfig) CertPEMBlock() []byte {
return cfg.certPEMBlock
}
// DevicesRunner returns a function to accept device connections.
// If adoptLst is not nil it will be used as the underlying listener, instead
// of creating one, wrapped in a TLS layer.
func DevicesRunner(adoptLst net.Listener, session func(net.Conn) error, logger logger.Logger, parsedCfg *DevicesParsedConfig) func() {
BootLogger.Debugf("PingInterval: %s, ExchangeTimeout %s", parsedCfg.PingInterval(), parsedCfg.ExchangeTimeout())
var rlim syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim)
if err != nil {
BootLogFatalf("getrlimit failed: %v", err)
}
BootLogger.Debugf("nofile soft: %d hard: %d", rlim.Cur, rlim.Max)
lst, err := listener.DeviceListen(adoptLst, parsedCfg)
if err != nil {
BootLogFatalf("start device listening: %v", err)
}
BootLogListener("devices", lst)
return func() {
err = lst.AcceptLoop(session, logger)
if err != nil {
BootLogFatalf("accepting device connections: %v", err)
}
}
}
ubuntu-push-0.2+14.04.20140411/server/doc.go 0000644 0000153 0177776 00000001335 12322032412 020637 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package server contains code to start server components hosted
// by the subpackages.
package server
ubuntu-push-0.2+14.04.20140411/server/dev/ 0000755 0000153 0177776 00000000000 12322032700 020317 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/dev/server.go 0000644 0000153 0177776 00000005424 12322032421 022161 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Dev is a simple development server.
package main
import (
"encoding/json"
"net"
"net/http"
"os"
"path/filepath"
"launchpad.net/ubuntu-push/config"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/server"
"launchpad.net/ubuntu-push/server/api"
"launchpad.net/ubuntu-push/server/broker/simple"
"launchpad.net/ubuntu-push/server/session"
"launchpad.net/ubuntu-push/server/store"
)
type configuration struct {
// device server configuration
server.DevicesParsedConfig
// api http server configuration
server.HTTPServeParsedConfig
// delivery domain
DeliveryDomain string `json:"delivery_domain"`
}
func main() {
cfgFpaths := os.Args[1:]
cfg := &configuration{}
err := config.ReadFiles(cfg, cfgFpaths...)
if err != nil {
server.BootLogFatalf("reading config: %v", err)
}
err = cfg.DevicesParsedConfig.FinishLoad(filepath.Dir(cfgFpaths[len(cfgFpaths)-1]))
if err != nil {
server.BootLogFatalf("reading config: %v", err)
}
logger := logger.NewSimpleLogger(os.Stderr, "debug")
// setup a pending store and start the broker
sto := store.NewInMemoryPendingStore()
broker := simple.NewSimpleBroker(sto, cfg, logger)
broker.Start()
defer broker.Stop()
// serve the http api
storeForRequest := func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return sto, nil
}
lst, err := net.Listen("tcp", cfg.Addr())
if err != nil {
server.BootLogFatalf("start device listening: %v", err)
}
mux := api.MakeHandlersMux(storeForRequest, broker, logger)
// & /delivery-hosts
mux.HandleFunc("/delivery-hosts", func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.Encode(map[string]interface{}{
"hosts": []string{lst.Addr().String()},
"domain": cfg.DeliveryDomain,
})
})
handler := api.PanicTo500Handler(mux, logger)
go server.HTTPServeRunner(nil, handler, &cfg.HTTPServeParsedConfig)()
// listen for device connections
server.DevicesRunner(lst, func(conn net.Conn) error {
track := session.NewTracker(logger)
return session.Session(conn, broker, cfg, track)
}, logger, &cfg.DevicesParsedConfig)()
}
ubuntu-push-0.2+14.04.20140411/server/broker/ 0000755 0000153 0177776 00000000000 12322032700 021025 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/broker/exchanges_test.go 0000644 0000153 0177776 00000017437 12322032421 024374 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package broker_test // use a package test to avoid cyclic imports
import (
"encoding/json"
"fmt"
"strings"
stdtesting "testing"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/server/broker"
"launchpad.net/ubuntu-push/server/broker/testing"
"launchpad.net/ubuntu-push/server/store"
)
func TestBroker(t *stdtesting.T) { TestingT(t) }
type exchangesSuite struct{}
var _ = Suite(&exchangesSuite{})
func (s *exchangesSuite) TestBroadcastExchangeInit(c *C) {
exchg := &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: 3,
NotificationPayloads: []json.RawMessage{
json.RawMessage(`{"a":"x"}`),
json.RawMessage(`[]`),
json.RawMessage(`{"a":"y"}`),
},
}
exchg.Init()
c.Check(exchg.Decoded, DeepEquals, []map[string]interface{}{
map[string]interface{}{"a": "x"},
nil,
map[string]interface{}{"a": "y"},
})
}
func (s *exchangesSuite) TestBroadcastExchange(c *C) {
sess := &testing.TestBrokerSession{
LevelsMap: broker.LevelsMap(map[store.InternalChannelId]int64{}),
Model: "m1",
ImageChannel: "img1",
}
exchg := &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: 3,
NotificationPayloads: []json.RawMessage{
json.RawMessage(`{"img1/m1":100}`),
json.RawMessage(`{"img2/m2":200}`),
},
}
exchg.Init()
outMsg, inMsg, err := exchg.Prepare(sess)
c.Assert(err, IsNil)
// check
marshalled, err := json.Marshal(outMsg)
c.Assert(err, IsNil)
c.Check(string(marshalled), Equals, `{"T":"broadcast","ChanId":"0","TopLevel":3,"Payloads":[{"img1/m1":100}]}`)
err = json.Unmarshal([]byte(`{"T":"ack"}`), inMsg)
c.Assert(err, IsNil)
err = exchg.Acked(sess, true)
c.Assert(err, IsNil)
c.Check(sess.LevelsMap[store.SystemInternalChannelId], Equals, int64(3))
}
func (s *exchangesSuite) TestBroadcastExchangeEmpty(c *C) {
sess := &testing.TestBrokerSession{
LevelsMap: broker.LevelsMap(map[store.InternalChannelId]int64{}),
Model: "m1",
ImageChannel: "img1",
}
exchg := &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: 3,
NotificationPayloads: []json.RawMessage{},
}
exchg.Init()
outMsg, inMsg, err := exchg.Prepare(sess)
c.Assert(err, Equals, broker.ErrNop)
c.Check(outMsg, IsNil)
c.Check(inMsg, IsNil)
}
func (s *exchangesSuite) TestBroadcastExchangeEmptyButAhead(c *C) {
sess := &testing.TestBrokerSession{
LevelsMap: broker.LevelsMap(map[store.InternalChannelId]int64{
store.SystemInternalChannelId: 10,
}),
Model: "m1",
ImageChannel: "img1",
}
exchg := &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: 3,
NotificationPayloads: []json.RawMessage{},
}
exchg.Init()
outMsg, inMsg, err := exchg.Prepare(sess)
c.Assert(err, IsNil)
c.Check(outMsg, NotNil)
c.Check(inMsg, NotNil)
}
func (s *exchangesSuite) TestBroadcastExchangeReuseVsSplit(c *C) {
sess := &testing.TestBrokerSession{
LevelsMap: broker.LevelsMap(map[store.InternalChannelId]int64{}),
Model: "m1",
ImageChannel: "img1",
}
payloadFmt := fmt.Sprintf(`{"img1/m1":%%d,"bloat":"%s"}`, strings.Repeat("x", 1024*2))
needsSplitting := make([]json.RawMessage, 32)
for i := 0; i < 32; i++ {
needsSplitting[i] = json.RawMessage(fmt.Sprintf(payloadFmt, i))
}
topLevel := int64(len(needsSplitting))
exchg := &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: topLevel,
NotificationPayloads: needsSplitting,
}
exchg.Init()
outMsg, _, err := exchg.Prepare(sess)
c.Assert(err, IsNil)
parts := 0
for {
done := outMsg.Split()
parts++
if done {
break
}
}
c.Assert(parts, Equals, 2)
exchg = &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: topLevel + 2,
NotificationPayloads: []json.RawMessage{
json.RawMessage(`{"img1/m1":"x"}`),
json.RawMessage(`{"img1/m1":"y"}`),
},
}
exchg.Init()
outMsg, _, err = exchg.Prepare(sess)
c.Assert(err, IsNil)
done := outMsg.Split() // shouldn't panic
c.Check(done, Equals, true)
}
func (s *exchangesSuite) TestBroadcastExchangeAckMismatch(c *C) {
sess := &testing.TestBrokerSession{
LevelsMap: broker.LevelsMap(map[store.InternalChannelId]int64{}),
Model: "m1",
ImageChannel: "img2",
}
exchg := &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: 3,
NotificationPayloads: []json.RawMessage{
json.RawMessage(`{"img2/m1":1}`),
},
}
exchg.Init()
outMsg, inMsg, err := exchg.Prepare(sess)
c.Assert(err, IsNil)
// check
marshalled, err := json.Marshal(outMsg)
c.Assert(err, IsNil)
c.Check(string(marshalled), Equals, `{"T":"broadcast","ChanId":"0","TopLevel":3,"Payloads":[{"img2/m1":1}]}`)
err = json.Unmarshal([]byte(`{}`), inMsg)
c.Assert(err, IsNil)
err = exchg.Acked(sess, true)
c.Assert(err, Not(IsNil))
c.Check(sess.LevelsMap[store.SystemInternalChannelId], Equals, int64(0))
}
func (s *exchangesSuite) TestBroadcastExchangeFilterByLevel(c *C) {
sess := &testing.TestBrokerSession{
LevelsMap: broker.LevelsMap(map[store.InternalChannelId]int64{
store.SystemInternalChannelId: 2,
}),
Model: "m1",
ImageChannel: "img1",
}
exchg := &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: 3,
NotificationPayloads: []json.RawMessage{
json.RawMessage(`{"img1/m1":100}`),
json.RawMessage(`{"img1/m1":101}`),
},
}
exchg.Init()
outMsg, inMsg, err := exchg.Prepare(sess)
c.Assert(err, IsNil)
// check
marshalled, err := json.Marshal(outMsg)
c.Assert(err, IsNil)
c.Check(string(marshalled), Equals, `{"T":"broadcast","ChanId":"0","TopLevel":3,"Payloads":[{"img1/m1":101}]}`)
err = json.Unmarshal([]byte(`{"T":"ack"}`), inMsg)
c.Assert(err, IsNil)
err = exchg.Acked(sess, true)
c.Assert(err, IsNil)
}
func (s *exchangesSuite) TestBroadcastExchangeChannelFilter(c *C) {
sess := &testing.TestBrokerSession{
LevelsMap: broker.LevelsMap(map[store.InternalChannelId]int64{}),
Model: "m1",
ImageChannel: "img1",
}
exchg := &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: 5,
NotificationPayloads: []json.RawMessage{
json.RawMessage(`{"img1/m1":100}`),
json.RawMessage(`{"img2/m2":200}`),
json.RawMessage(`{"img1/m1":101}`),
},
}
exchg.Init()
outMsg, inMsg, err := exchg.Prepare(sess)
c.Assert(err, IsNil)
// check
marshalled, err := json.Marshal(outMsg)
c.Assert(err, IsNil)
c.Check(string(marshalled), Equals, `{"T":"broadcast","ChanId":"0","TopLevel":5,"Payloads":[{"img1/m1":100},{"img1/m1":101}]}`)
err = json.Unmarshal([]byte(`{"T":"ack"}`), inMsg)
c.Assert(err, IsNil)
err = exchg.Acked(sess, true)
c.Assert(err, IsNil)
c.Check(sess.LevelsMap[store.SystemInternalChannelId], Equals, int64(5))
}
func (s *exchangesSuite) TestConnBrokenExchange(c *C) {
sess := &testing.TestBrokerSession{}
cbe := &broker.ConnBrokenExchange{"REASON"}
outMsg, inMsg, err := cbe.Prepare(sess)
c.Assert(err, IsNil)
c.Check(inMsg, IsNil) // no answer is expected
// check
marshalled, err := json.Marshal(outMsg)
c.Assert(err, IsNil)
c.Check(string(marshalled), Equals, `{"T":"connbroken","Reason":"REASON"}`)
c.Check(func() { cbe.Acked(nil, true) }, PanicMatches, "Acked should not get invoked on ConnBrokenExchange")
}
ubuntu-push-0.2+14.04.20140411/server/broker/exchg_impl_test.go 0000644 0000153 0177776 00000005331 12322032412 024534 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package broker
import (
"encoding/json"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/server/store"
)
type exchangesImplSuite struct{}
var _ = Suite(&exchangesImplSuite{})
func (s *exchangesImplSuite) TestFilterByLevel(c *C) {
payloads := []json.RawMessage{
json.RawMessage(`{"a": 3}`),
json.RawMessage(`{"a": 4}`),
json.RawMessage(`{"a": 5}`),
}
res := filterByLevel(5, 5, payloads)
c.Check(len(res), Equals, 0)
res = filterByLevel(4, 5, payloads)
c.Check(len(res), Equals, 1)
c.Check(res[0], DeepEquals, json.RawMessage(`{"a": 5}`))
res = filterByLevel(3, 5, payloads)
c.Check(len(res), Equals, 2)
c.Check(res[0], DeepEquals, json.RawMessage(`{"a": 4}`))
res = filterByLevel(2, 5, payloads)
c.Check(len(res), Equals, 3)
res = filterByLevel(1, 5, payloads)
c.Check(len(res), Equals, 3)
// too ahead, pick only last
res = filterByLevel(10, 5, payloads)
c.Check(len(res), Equals, 1)
c.Check(res[0], DeepEquals, json.RawMessage(`{"a": 5}`))
}
func (s *exchangesImplSuite) TestFilterByLevelEmpty(c *C) {
res := filterByLevel(5, 0, nil)
c.Check(len(res), Equals, 0)
res = filterByLevel(5, 10, nil)
c.Check(len(res), Equals, 0)
}
func (s *exchangesImplSuite) TestChannelFilter(c *C) {
payloads := []json.RawMessage{
json.RawMessage(`{"a/x": 3}`),
json.RawMessage(`{"b/x": 4}`),
json.RawMessage(`{"a/y": 5}`),
json.RawMessage(`{"a/x": 6}`),
}
decoded := make([]map[string]interface{}, 4)
for i, p := range payloads {
err := json.Unmarshal(p, &decoded[i])
c.Assert(err, IsNil)
}
other := store.InternalChannelId("1")
c.Check(channelFilter("", store.SystemInternalChannelId, nil, nil), IsNil)
c.Check(channelFilter("", other, payloads[1:], decoded), DeepEquals, payloads[1:])
// use tag when channel is the sytem channel
c.Check(channelFilter("c/z", store.SystemInternalChannelId, payloads, decoded), HasLen, 0)
c.Check(channelFilter("a/x", store.SystemInternalChannelId, payloads, decoded), DeepEquals, []json.RawMessage{payloads[0], payloads[3]})
c.Check(channelFilter("a/x", store.SystemInternalChannelId, payloads[1:], decoded), DeepEquals, []json.RawMessage{payloads[3]})
}
ubuntu-push-0.2+14.04.20140411/server/broker/simple/ 0000755 0000153 0177776 00000000000 12322032700 022316 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/broker/simple/simple_test.go 0000644 0000153 0177776 00000005046 12322032412 025202 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package simple
import (
"encoding/json"
stdtesting "testing"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/server/broker"
"launchpad.net/ubuntu-push/server/broker/testing"
"launchpad.net/ubuntu-push/server/store"
)
func TestSimple(t *stdtesting.T) { TestingT(t) }
type simpleSuite struct{}
var _ = Suite(&simpleSuite{})
var testBrokerConfig = &testing.TestBrokerConfig{10, 5}
func (s *simpleSuite) TestNew(c *C) {
sto := store.NewInMemoryPendingStore()
b := NewSimpleBroker(sto, testBrokerConfig, nil)
c.Check(cap(b.sessionCh), Equals, 5)
c.Check(len(b.registry), Equals, 0)
c.Check(b.sto, Equals, sto)
}
func (s *simpleSuite) TestFeedPending(c *C) {
sto := store.NewInMemoryPendingStore()
muchLater := time.Now().Add(10 * time.Minute)
notification1 := json.RawMessage(`{"m": "M"}`)
decoded1 := map[string]interface{}{"m": "M"}
sto.AppendToChannel(store.SystemInternalChannelId, notification1, muchLater)
b := NewSimpleBroker(sto, testBrokerConfig, nil)
sess := &simpleBrokerSession{
exchanges: make(chan broker.Exchange, 1),
}
b.feedPending(sess)
c.Assert(len(sess.exchanges), Equals, 1)
exchg1 := <-sess.exchanges
c.Check(exchg1, DeepEquals, &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: 1,
NotificationPayloads: []json.RawMessage{notification1},
Decoded: []map[string]interface{}{decoded1},
})
}
func (s *simpleSuite) TestFeedPendingNop(c *C) {
sto := store.NewInMemoryPendingStore()
muchLater := time.Now().Add(10 * time.Minute)
notification1 := json.RawMessage(`{"m": "M"}`)
sto.AppendToChannel(store.SystemInternalChannelId, notification1, muchLater)
b := NewSimpleBroker(sto, testBrokerConfig, nil)
sess := &simpleBrokerSession{
exchanges: make(chan broker.Exchange, 1),
levels: map[store.InternalChannelId]int64{
store.SystemInternalChannelId: 1,
},
}
b.feedPending(sess)
c.Assert(len(sess.exchanges), Equals, 0)
}
ubuntu-push-0.2+14.04.20140411/server/broker/simple/simple.go 0000644 0000153 0177776 00000015412 12322032421 024141 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package simple implements a simple broker for just one process.
package simple
import (
"sync"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/protocol"
"launchpad.net/ubuntu-push/server/broker"
"launchpad.net/ubuntu-push/server/store"
)
// SimpleBroker implements broker.Broker/BrokerSending for everything
// in just one process.
type SimpleBroker struct {
sto store.PendingStore
logger logger.Logger
// running state
runMutex sync.Mutex
running bool
stop chan bool
stopped chan bool
// sessions
sessionCh chan *simpleBrokerSession
registry map[string]*simpleBrokerSession
sessionQueueSize uint
// delivery
deliveryCh chan *delivery
}
// simpleBrokerSession represents a session in the broker.
type simpleBrokerSession struct {
registered bool
deviceId string
model string
imageChannel string
done chan bool
exchanges chan broker.Exchange
levels broker.LevelsMap
// for exchanges
exchgScratch broker.ExchangesScratchArea
}
type deliveryKind int
const (
broadcastDelivery deliveryKind = iota
)
// delivery holds all the information to request a delivery
type delivery struct {
kind deliveryKind
chanId store.InternalChannelId
}
func (sess *simpleBrokerSession) SessionChannel() <-chan broker.Exchange {
return sess.exchanges
}
func (sess *simpleBrokerSession) DeviceIdentifier() string {
return sess.deviceId
}
func (sess *simpleBrokerSession) DeviceImageModel() string {
return sess.model
}
func (sess *simpleBrokerSession) DeviceImageChannel() string {
return sess.imageChannel
}
func (sess *simpleBrokerSession) Levels() broker.LevelsMap {
return sess.levels
}
func (sess *simpleBrokerSession) ExchangeScratchArea() *broker.ExchangesScratchArea {
return &sess.exchgScratch
}
// NewSimpleBroker makes a new SimpleBroker.
func NewSimpleBroker(sto store.PendingStore, cfg broker.BrokerConfig, logger logger.Logger) *SimpleBroker {
sessionCh := make(chan *simpleBrokerSession, cfg.BrokerQueueSize())
deliveryCh := make(chan *delivery, cfg.BrokerQueueSize())
registry := make(map[string]*simpleBrokerSession)
return &SimpleBroker{
logger: logger,
sto: sto,
stop: make(chan bool),
stopped: make(chan bool),
registry: registry,
sessionCh: sessionCh,
deliveryCh: deliveryCh,
sessionQueueSize: cfg.SessionQueueSize(),
}
}
// Start starts the broker.
func (b *SimpleBroker) Start() {
b.runMutex.Lock()
defer b.runMutex.Unlock()
if b.running {
return
}
b.running = true
go b.run()
}
// Stop stops the broker.
func (b *SimpleBroker) Stop() {
b.runMutex.Lock()
defer b.runMutex.Unlock()
if !b.running {
return
}
b.stop <- true
<-b.stopped
b.running = false
}
// Running returns whether ther broker is running.
func (b *SimpleBroker) Running() bool {
b.runMutex.Lock()
defer b.runMutex.Unlock()
return b.running
}
func (b *SimpleBroker) feedPending(sess *simpleBrokerSession) error {
// find relevant channels, for now only system
channels := []store.InternalChannelId{store.SystemInternalChannelId}
for _, chanId := range channels {
topLevel, payloads, err := b.sto.GetChannelSnapshot(chanId)
if err != nil {
// next broadcast will try again
b.logger.Errorf("unsuccessful feed pending, get channel snapshot for %v: %v", chanId, err)
continue
}
clientLevel := sess.levels[chanId]
if clientLevel != topLevel {
broadcastExchg := &broker.BroadcastExchange{
ChanId: chanId,
TopLevel: topLevel,
NotificationPayloads: payloads,
}
broadcastExchg.Init()
sess.exchanges <- broadcastExchg
}
}
return nil
}
// Register registers a session with the broker. It feeds the session
// pending notifications as well.
func (b *SimpleBroker) Register(connect *protocol.ConnectMsg) (broker.BrokerSession, error) {
// xxx sanity check DeviceId
model, err := broker.GetInfoString(connect, "device", "?")
if err != nil {
return nil, err
}
imageChannel, err := broker.GetInfoString(connect, "channel", "?")
if err != nil {
return nil, err
}
levels := map[store.InternalChannelId]int64{}
for hexId, v := range connect.Levels {
id, err := store.HexToInternalChannelId(hexId)
if err != nil {
return nil, &broker.ErrAbort{err.Error()}
}
levels[id] = v
}
sess := &simpleBrokerSession{
deviceId: connect.DeviceId,
model: model,
imageChannel: imageChannel,
done: make(chan bool),
exchanges: make(chan broker.Exchange, b.sessionQueueSize),
levels: levels,
}
b.sessionCh <- sess
<-sess.done
err = b.feedPending(sess)
if err != nil {
return nil, err
}
return sess, nil
}
// Unregister unregisters a session with the broker. Doesn't wait.
func (b *SimpleBroker) Unregister(s broker.BrokerSession) {
sess := s.(*simpleBrokerSession)
b.sessionCh <- sess
}
// run runs the agent logic of the broker.
func (b *SimpleBroker) run() {
Loop:
for {
select {
case <-b.stop:
b.stopped <- true
break Loop
case sess := <-b.sessionCh:
if sess.registered { // unregister
// unregister only current
if b.registry[sess.deviceId] == sess {
delete(b.registry, sess.deviceId)
}
} else { // register
prev := b.registry[sess.deviceId]
if prev != nil { // kick it
close(prev.exchanges)
}
b.registry[sess.deviceId] = sess
sess.registered = true
sess.done <- true
}
case delivery := <-b.deliveryCh:
switch delivery.kind {
case broadcastDelivery:
topLevel, payloads, err := b.sto.GetChannelSnapshot(delivery.chanId)
if err != nil {
// next broadcast will try again
b.logger.Errorf("unsuccessful broadcast, get channel snapshot for %v: %v", delivery.chanId, err)
continue Loop
}
broadcastExchg := &broker.BroadcastExchange{
ChanId: delivery.chanId,
TopLevel: topLevel,
NotificationPayloads: payloads,
}
broadcastExchg.Init()
for _, sess := range b.registry {
sess.exchanges <- broadcastExchg
}
}
}
}
}
// Broadcast requests the broadcast for a channel.
func (b *SimpleBroker) Broadcast(chanId store.InternalChannelId) {
b.deliveryCh <- &delivery{
kind: broadcastDelivery,
chanId: chanId,
}
}
ubuntu-push-0.2+14.04.20140411/server/broker/simple/suite_test.go 0000644 0000153 0177776 00000002764 12322032412 025046 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package simple
import (
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/server/broker"
"launchpad.net/ubuntu-push/server/broker/testsuite"
"launchpad.net/ubuntu-push/server/store"
)
// run the common broker test suite against SimpleBroker
// aliasing through embedding to get saner report names by gocheck
type commonBrokerSuite struct {
testsuite.CommonBrokerSuite
}
var _ = Suite(&commonBrokerSuite{testsuite.CommonBrokerSuite{
MakeBroker: func(sto store.PendingStore, cfg broker.BrokerConfig, log logger.Logger) testsuite.FullBroker {
return NewSimpleBroker(sto, cfg, log)
},
RevealSession: func(b broker.Broker, deviceId string) broker.BrokerSession {
return b.(*SimpleBroker).registry[deviceId]
},
RevealBroadcastExchange: func(exchg broker.Exchange) *broker.BroadcastExchange {
return exchg.(*broker.BroadcastExchange)
},
}})
ubuntu-push-0.2+14.04.20140411/server/broker/exchanges.go 0000644 0000153 0177776 00000010646 12322032421 023330 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package broker
import (
"encoding/json"
"fmt"
"launchpad.net/ubuntu-push/protocol"
"launchpad.net/ubuntu-push/server/store"
)
// Exchanges
// Scratch area for exchanges, sessions should hold one of these.
type ExchangesScratchArea struct {
broadcastMsg protocol.BroadcastMsg
ackMsg protocol.AckMsg
connBrokenMsg protocol.ConnBrokenMsg
}
// BroadcastExchange leads a session through delivering a BROADCAST.
// For simplicity it is fully public.
type BroadcastExchange struct {
ChanId store.InternalChannelId
TopLevel int64
NotificationPayloads []json.RawMessage
Decoded []map[string]interface{}
}
// check interface already here
var _ Exchange = (*BroadcastExchange)(nil)
// Init ensures the BroadcastExchange is fully initialized for the sessions.
func (sbe *BroadcastExchange) Init() {
decoded := make([]map[string]interface{}, len(sbe.NotificationPayloads))
sbe.Decoded = decoded
for i, p := range sbe.NotificationPayloads {
err := json.Unmarshal(p, &decoded[i])
if err != nil {
decoded[i] = nil
}
}
}
func filterByLevel(clientLevel, topLevel int64, payloads []json.RawMessage) []json.RawMessage {
c := int64(len(payloads))
if c == 0 {
return nil
}
delta := topLevel - clientLevel
if delta < 0 { // means too ahead, send the last pending
delta = 1
}
if delta < c {
return payloads[c-delta:]
} else {
return payloads
}
}
func channelFilter(tag string, chanId store.InternalChannelId, payloads []json.RawMessage, decoded []map[string]interface{}) []json.RawMessage {
if len(payloads) != 0 && chanId == store.SystemInternalChannelId {
decoded := decoded[len(decoded)-len(payloads):]
filtered := make([]json.RawMessage, 0)
for i, decoded1 := range decoded {
if _, ok := decoded1[tag]; ok {
filtered = append(filtered, payloads[i])
}
}
payloads = filtered
}
return payloads
}
// Prepare session for a BROADCAST.
func (sbe *BroadcastExchange) Prepare(sess BrokerSession) (outMessage protocol.SplittableMsg, inMessage interface{}, err error) {
clientLevel := sess.Levels()[sbe.ChanId]
payloads := filterByLevel(clientLevel, sbe.TopLevel, sbe.NotificationPayloads)
tag := fmt.Sprintf("%s/%s", sess.DeviceImageChannel(), sess.DeviceImageModel())
payloads = channelFilter(tag, sbe.ChanId, payloads, sbe.Decoded)
if len(payloads) == 0 && sbe.TopLevel >= clientLevel {
// empty and don't need to force resync => do nothing
return nil, nil, ErrNop
}
scratchArea := sess.ExchangeScratchArea()
scratchArea.broadcastMsg.Reset()
scratchArea.broadcastMsg.Type = "broadcast"
// xxx need an AppId as well, later
scratchArea.broadcastMsg.ChanId = store.InternalChannelIdToHex(sbe.ChanId)
scratchArea.broadcastMsg.TopLevel = sbe.TopLevel
scratchArea.broadcastMsg.Payloads = payloads
return &scratchArea.broadcastMsg, &scratchArea.ackMsg, nil
}
// Acked deals with an ACK for a BROADCAST.
func (sbe *BroadcastExchange) Acked(sess BrokerSession, done bool) error {
scratchArea := sess.ExchangeScratchArea()
if scratchArea.ackMsg.Type != "ack" {
return &ErrAbort{"expected ACK message"}
}
// update levels
sess.Levels()[sbe.ChanId] = sbe.TopLevel
return nil
}
// ConnBrokenExchange breaks a session giving a reason.
type ConnBrokenExchange struct {
Reason string
}
// check interface already here
var _ Exchange = (*ConnBrokenExchange)(nil)
// Prepare session for a CONNBROKEN.
func (cbe *ConnBrokenExchange) Prepare(sess BrokerSession) (outMessage protocol.SplittableMsg, inMessage interface{}, err error) {
scratchArea := sess.ExchangeScratchArea()
scratchArea.connBrokenMsg.Type = "connbroken"
scratchArea.connBrokenMsg.Reason = cbe.Reason
return &scratchArea.connBrokenMsg, nil, nil
}
// CONNBROKEN isn't acked
func (cbe *ConnBrokenExchange) Acked(sess BrokerSession, done bool) error {
panic("Acked should not get invoked on ConnBrokenExchange")
}
ubuntu-push-0.2+14.04.20140411/server/broker/broker_test.go 0000644 0000153 0177776 00000002570 12322032412 023703 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package broker
import (
"fmt"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/protocol"
)
type brokerSuite struct{}
var _ = Suite(&brokerSuite{})
func (s *brokerSuite) TestErrAbort(c *C) {
err := &ErrAbort{"expected FOO"}
c.Check(fmt.Sprintf("%s", err), Equals, "session aborted (expected FOO)")
}
func (s *brokerSuite) TestGetInfoString(c *C) {
connectMsg := &protocol.ConnectMsg{}
v, err := GetInfoString(connectMsg, "foo", "?")
c.Check(err, IsNil)
c.Check(v, Equals, "?")
connectMsg.Info = map[string]interface{}{"foo": "yay"}
v, err = GetInfoString(connectMsg, "foo", "?")
c.Check(err, IsNil)
c.Check(v, Equals, "yay")
connectMsg.Info["foo"] = 33
v, err = GetInfoString(connectMsg, "foo", "?")
c.Check(err, Equals, ErrUnexpectedValue)
}
ubuntu-push-0.2+14.04.20140411/server/broker/testsuite/ 0000755 0000153 0177776 00000000000 12322032700 023056 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/broker/testsuite/suite.go 0000644 0000153 0177776 00000020426 12322032421 024542 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package testsuite contains a common test suite for brokers.
package testsuite
import (
"encoding/json"
"errors"
// "log"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/protocol"
"launchpad.net/ubuntu-push/server/broker"
"launchpad.net/ubuntu-push/server/broker/testing"
"launchpad.net/ubuntu-push/server/store"
helpers "launchpad.net/ubuntu-push/testing"
)
// The expected interface for tested brokers.
type FullBroker interface {
broker.Broker
broker.BrokerSending
Start()
Stop()
Running() bool
}
// The common brokers' test suite.
type CommonBrokerSuite struct {
// Build the broker for testing.
MakeBroker func(store.PendingStore, broker.BrokerConfig, logger.Logger) FullBroker
// Let us get to a session under the broker.
RevealSession func(broker.Broker, string) broker.BrokerSession
// Let us get to a broker.BroadcastExchange from an Exchange.
RevealBroadcastExchange func(broker.Exchange) *broker.BroadcastExchange
// private
testlog *helpers.TestLogger
}
func (s *CommonBrokerSuite) SetUpTest(c *C) {
s.testlog = helpers.NewTestLogger(c, "error")
}
var testBrokerConfig = &testing.TestBrokerConfig{10, 5}
func (s *CommonBrokerSuite) TestSanity(c *C) {
sto := store.NewInMemoryPendingStore()
b := s.MakeBroker(sto, testBrokerConfig, nil)
c.Check(s.RevealSession(b, "FOO"), IsNil)
}
func (s *CommonBrokerSuite) TestStartStop(c *C) {
b := s.MakeBroker(nil, testBrokerConfig, nil)
b.Start()
c.Check(b.Running(), Equals, true)
b.Start()
b.Stop()
c.Check(b.Running(), Equals, false)
b.Stop()
}
func (s *CommonBrokerSuite) TestRegistration(c *C) {
sto := store.NewInMemoryPendingStore()
b := s.MakeBroker(sto, testBrokerConfig, nil)
b.Start()
defer b.Stop()
sess, err := b.Register(&protocol.ConnectMsg{
Type: "connect",
DeviceId: "dev-1",
Levels: map[string]int64{"0": 5},
Info: map[string]interface{}{
"device": "model",
"channel": "daily",
},
})
c.Assert(err, IsNil)
c.Assert(s.RevealSession(b, "dev-1"), Equals, sess)
c.Assert(sess.DeviceIdentifier(), Equals, "dev-1")
c.Check(sess.DeviceImageModel(), Equals, "model")
c.Check(sess.DeviceImageChannel(), Equals, "daily")
c.Assert(sess.ExchangeScratchArea(), Not(IsNil))
c.Check(sess.Levels(), DeepEquals, broker.LevelsMap(map[store.InternalChannelId]int64{
store.SystemInternalChannelId: 5,
}))
b.Unregister(sess)
// just to make sure the unregister was processed
_, err = b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: ""})
c.Assert(err, IsNil)
c.Check(s.RevealSession(b, "dev-1"), IsNil)
}
func (s *CommonBrokerSuite) TestRegistrationBrokenLevels(c *C) {
sto := store.NewInMemoryPendingStore()
b := s.MakeBroker(sto, testBrokerConfig, nil)
b.Start()
defer b.Stop()
_, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1", Levels: map[string]int64{"z": 5}})
c.Check(err, FitsTypeOf, &broker.ErrAbort{})
}
func (s *CommonBrokerSuite) TestRegistrationInfoErrors(c *C) {
sto := store.NewInMemoryPendingStore()
b := s.MakeBroker(sto, testBrokerConfig, nil)
b.Start()
defer b.Stop()
info := map[string]interface{}{
"device": -1,
}
_, err := b.Register(&protocol.ConnectMsg{Type: "connect", Info: info})
c.Check(err, Equals, broker.ErrUnexpectedValue)
info["device"] = "m"
info["channel"] = -1
_, err = b.Register(&protocol.ConnectMsg{Type: "connect", Info: info})
c.Check(err, Equals, broker.ErrUnexpectedValue)
}
func (s *CommonBrokerSuite) TestRegistrationFeedPending(c *C) {
sto := store.NewInMemoryPendingStore()
notification1 := json.RawMessage(`{"m": "M"}`)
muchLater := time.Now().Add(10 * time.Minute)
sto.AppendToChannel(store.SystemInternalChannelId, notification1, muchLater)
b := s.MakeBroker(sto, testBrokerConfig, nil)
b.Start()
defer b.Stop()
sess, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1"})
c.Assert(err, IsNil)
c.Check(len(sess.SessionChannel()), Equals, 1)
}
func (s *CommonBrokerSuite) TestRegistrationFeedPendingError(c *C) {
sto := &testFailingStore{}
b := s.MakeBroker(sto, testBrokerConfig, s.testlog)
b.Start()
defer b.Stop()
_, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1"})
c.Assert(err, IsNil)
// but
c.Check(s.testlog.Captured(), Matches, "ERROR unsuccessful feed pending, get channel snapshot for 0: get channel snapshot fail\n")
}
func (s *CommonBrokerSuite) TestRegistrationLastWins(c *C) {
sto := store.NewInMemoryPendingStore()
b := s.MakeBroker(sto, testBrokerConfig, nil)
b.Start()
defer b.Stop()
sess1, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1"})
c.Assert(err, IsNil)
sess2, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1"})
c.Assert(err, IsNil)
checkAndFalse := false
// previous session got signaled by closing its channel
select {
case _, ok := <-sess1.SessionChannel():
checkAndFalse = ok == false
default:
}
c.Check(checkAndFalse, Equals, true)
c.Assert(s.RevealSession(b, "dev-1"), Equals, sess2)
b.Unregister(sess1)
// just to make sure the unregister was processed
_, err = b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: ""})
c.Assert(err, IsNil)
c.Check(s.RevealSession(b, "dev-1"), Equals, sess2)
}
func (s *CommonBrokerSuite) TestBroadcast(c *C) {
sto := store.NewInMemoryPendingStore()
notification1 := json.RawMessage(`{"m": "M"}`)
decoded1 := map[string]interface{}{"m": "M"}
b := s.MakeBroker(sto, testBrokerConfig, nil)
b.Start()
defer b.Stop()
sess1, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1"})
c.Assert(err, IsNil)
sess2, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-2"})
c.Assert(err, IsNil)
// add notification to channel *after* the registrations
muchLater := time.Now().Add(10 * time.Minute)
sto.AppendToChannel(store.SystemInternalChannelId, notification1, muchLater)
b.Broadcast(store.SystemInternalChannelId)
select {
case <-time.After(5 * time.Second):
c.Fatal("taking too long to get broadcast exchange")
case exchg1 := <-sess1.SessionChannel():
c.Check(s.RevealBroadcastExchange(exchg1), DeepEquals, &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: 1,
NotificationPayloads: []json.RawMessage{notification1},
Decoded: []map[string]interface{}{decoded1},
})
}
select {
case <-time.After(5 * time.Second):
c.Fatal("taking too long to get broadcast exchange")
case exchg2 := <-sess2.SessionChannel():
c.Check(s.RevealBroadcastExchange(exchg2), DeepEquals, &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: 1,
NotificationPayloads: []json.RawMessage{notification1},
Decoded: []map[string]interface{}{decoded1},
})
}
}
type testFailingStore struct {
store.InMemoryPendingStore
countdownToFail int
}
func (sto *testFailingStore) GetChannelSnapshot(chanId store.InternalChannelId) (int64, []json.RawMessage, error) {
if sto.countdownToFail == 0 {
return 0, nil, errors.New("get channel snapshot fail")
}
sto.countdownToFail--
return 0, nil, nil
}
func (s *CommonBrokerSuite) TestBroadcastFail(c *C) {
logged := make(chan bool, 1)
s.testlog.SetLogEventCb(func(string) {
logged <- true
})
sto := &testFailingStore{countdownToFail: 1}
b := s.MakeBroker(sto, testBrokerConfig, s.testlog)
b.Start()
defer b.Stop()
_, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1"})
c.Assert(err, IsNil)
b.Broadcast(store.SystemInternalChannelId)
select {
case <-time.After(5 * time.Second):
c.Fatal("taking too long to log error")
case <-logged:
}
c.Check(s.testlog.Captured(), Matches, "ERROR unsuccessful broadcast, get channel snapshot for 0: get channel snapshot fail\n")
}
ubuntu-push-0.2+14.04.20140411/server/broker/broker.go 0000644 0000153 0177776 00000006223 12322032421 022643 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package broker handles session registrations and delivery of messages
// through sessions.
package broker
import (
"errors"
"fmt"
"launchpad.net/ubuntu-push/protocol"
"launchpad.net/ubuntu-push/server/store"
)
// Broker is responsible for registring sessions and delivering messages
// through them.
type Broker interface {
// Register the session.
Register(*protocol.ConnectMsg) (BrokerSession, error)
// Unregister the session.
Unregister(BrokerSession)
}
// BrokerSending is the notification sending facet of the broker.
type BrokerSending interface {
// Broadcast channel.
Broadcast(chanId store.InternalChannelId)
}
// Exchange leads the session through performing an exchange, typically delivery.
type Exchange interface {
Prepare(sess BrokerSession) (outMessage protocol.SplittableMsg, inMessage interface{}, err error)
Acked(sess BrokerSession, done bool) error
}
// ErrNop returned by Prepare means nothing to do/send.
var ErrNop = errors.New("nothing to send")
// LevelsMap is the type for holding channel levels for session.
type LevelsMap map[store.InternalChannelId]int64
// GetInfoString helps retrivieng a string out of a protocol.ConnectMsg.Info
func GetInfoString(msg *protocol.ConnectMsg, name, defaultVal string) (string, error) {
v, ok := msg.Info[name]
if !ok {
return defaultVal, nil
}
s, ok := v.(string)
if !ok {
return "", ErrUnexpectedValue
}
return s, nil
}
// BrokerSession holds broker session state.
type BrokerSession interface {
// SessionChannel returns the session control channel
// on which the session gets exchanges to perform.
SessionChannel() <-chan Exchange
// DeviceIdentifier returns the device id string.
DeviceIdentifier() string
// DeviceImageModel returns the device model.
DeviceImageModel() string
// DeviceImageChannel returns the device system image channel.
DeviceImageChannel() string
// Levels returns the current channel levels for the session
Levels() LevelsMap
// ExchangeScratchArea returns the scratch area for exchanges.
ExchangeScratchArea() *ExchangesScratchArea
}
// Session aborted error.
type ErrAbort struct {
Reason string
}
func (ea *ErrAbort) Error() string {
return fmt.Sprintf("session aborted (%s)", ea.Reason)
}
// Unexpect value in message
var ErrUnexpectedValue = &ErrAbort{"unexpected value in message"}
// BrokerConfig gives access to the typical broker configuration.
type BrokerConfig interface {
// SessionQueueSize gives the session queue size.
SessionQueueSize() uint
// BrokerQueueSize gives the internal broker queue size.
BrokerQueueSize() uint
}
ubuntu-push-0.2+14.04.20140411/server/broker/testing/ 0000755 0000153 0177776 00000000000 12322032700 022502 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/broker/testing/impls.go 0000644 0000153 0177776 00000003537 12322032412 024165 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package testing contains simple test implementations of some broker interfaces.
package testing
import (
"launchpad.net/ubuntu-push/server/broker"
)
// Test implementation of BrokerSession.
type TestBrokerSession struct {
DeviceId string
Model string
ImageChannel string
Exchanges chan broker.Exchange
LevelsMap broker.LevelsMap
exchgScratch broker.ExchangesScratchArea
}
func (tbs *TestBrokerSession) DeviceIdentifier() string {
return tbs.DeviceId
}
func (tbs *TestBrokerSession) DeviceImageModel() string {
return tbs.Model
}
func (tbs *TestBrokerSession) DeviceImageChannel() string {
return tbs.ImageChannel
}
func (tbs *TestBrokerSession) SessionChannel() <-chan broker.Exchange {
return tbs.Exchanges
}
func (tbs *TestBrokerSession) Levels() broker.LevelsMap {
return tbs.LevelsMap
}
func (tbs *TestBrokerSession) ExchangeScratchArea() *broker.ExchangesScratchArea {
return &tbs.exchgScratch
}
// Test implementation of BrokerConfig.
type TestBrokerConfig struct {
ConfigSessionQueueSize uint
ConfigBrokerQueueSize uint
}
func (tbc *TestBrokerConfig) SessionQueueSize() uint {
return tbc.ConfigSessionQueueSize
}
func (tbc *TestBrokerConfig) BrokerQueueSize() uint {
return tbc.ConfigBrokerQueueSize
}
ubuntu-push-0.2+14.04.20140411/server/api/ 0000755 0000153 0177776 00000000000 12322032700 020312 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/api/middleware_test.go 0000644 0000153 0177776 00000002616 12322032412 024022 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package api
import (
"net/http"
"net/http/httptest"
. "launchpad.net/gocheck"
helpers "launchpad.net/ubuntu-push/testing"
)
type middlewareSuite struct{}
var _ = Suite(&middlewareSuite{})
func (s *middlewareSuite) TestPanicTo500Handler(c *C) {
logger := helpers.NewTestLogger(c, "debug")
panicking := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
panic("panic in handler")
})
h := PanicTo500Handler(panicking, logger)
w := httptest.NewRecorder()
h.ServeHTTP(w, nil)
c.Check(w.Code, Equals, 500)
c.Check(logger.Captured(), Matches, "(?s)ERROR\\(PANIC\\) serving http: panic in handler:.*")
c.Check(w.Header().Get("Content-Type"), Equals, "application/json")
c.Check(w.Body.String(), Equals, `{"error":"internal","message":"INTERNAL SERVER ERROR"}`)
}
ubuntu-push-0.2+14.04.20140411/server/api/handlers.go 0000644 0000153 0177776 00000017047 12322032412 022452 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package api has code that offers a REST API for the applications that
// want to push messages.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/server/broker"
"launchpad.net/ubuntu-push/server/store"
)
const MaxRequestBodyBytes = 4 * 1024
const JSONMediaType = "application/json"
// APIError represents a API error (both internally and as JSON in a response).
type APIError struct {
// http status code
StatusCode int `json:"-"`
// machine readable label
ErrorLabel string `json:"error"`
// human message
Message string `json:"message"`
}
// machine readable error labels
const (
ioError = "io-error"
invalidRequest = "invalid-request"
unknownChannel = "unknown-channel"
unavailable = "unavailable"
internalError = "internal"
)
func (apiErr *APIError) Error() string {
return fmt.Sprintf("api %s: %s", apiErr.ErrorLabel, apiErr.Message)
}
// Well-known prebuilt API errors
var (
ErrNoContentLengthProvided = &APIError{
http.StatusLengthRequired,
invalidRequest,
"A Content-Length must be provided",
}
ErrRequestBodyEmpty = &APIError{
http.StatusBadRequest,
invalidRequest,
"Request body empty",
}
ErrRequestBodyTooLarge = &APIError{
http.StatusRequestEntityTooLarge,
invalidRequest,
"Request body too large",
}
ErrWrongContentType = &APIError{
http.StatusUnsupportedMediaType,
invalidRequest,
"Wrong content type, should be application/json",
}
ErrWrongRequestMethod = &APIError{
http.StatusMethodNotAllowed,
invalidRequest,
"Wrong request method, should be POST",
}
ErrMalformedJSONObject = &APIError{
http.StatusBadRequest,
invalidRequest,
"Malformed JSON Object",
}
ErrCouldNotReadBody = &APIError{
http.StatusBadRequest,
ioError,
"Could not read request body",
}
ErrMissingData = &APIError{
http.StatusBadRequest,
invalidRequest,
"Missing data field",
}
ErrInvalidExpiration = &APIError{
http.StatusBadRequest,
invalidRequest,
"Invalid expiration date",
}
ErrPastExpiration = &APIError{
http.StatusBadRequest,
invalidRequest,
"Past expiration date",
}
ErrUnknownChannel = &APIError{
http.StatusBadRequest,
unknownChannel,
"Unknown channel",
}
ErrUnknown = &APIError{
http.StatusInternalServerError,
internalError,
"Unknown error",
}
ErrStoreUnavailable = &APIError{
http.StatusServiceUnavailable,
unavailable,
"Message store unavailable",
}
ErrCouldNotStoreNotification = &APIError{
http.StatusServiceUnavailable,
unavailable,
"Could not store notification",
}
)
type Message struct {
Registration string `json:"registration"`
CoalesceTag string `json:"coalesce_tag"`
Data json.RawMessage `json:"data"`
}
// Broadcast request JSON object.
type Broadcast struct {
Channel string `json:"channel"`
ExpireOn string `json:"expire_on"`
Data json.RawMessage `json:"data"`
}
// RespondError writes back a JSON error response for a APIError.
func RespondError(writer http.ResponseWriter, apiErr *APIError) {
wireError, err := json.Marshal(apiErr)
if err != nil {
panic(fmt.Errorf("couldn't marshal our own errors: %v", err))
}
writer.Header().Set("Content-type", JSONMediaType)
writer.WriteHeader(apiErr.StatusCode)
writer.Write(wireError)
}
func checkContentLength(request *http.Request, maxBodySize int64) *APIError {
if request.ContentLength == -1 {
return ErrNoContentLengthProvided
}
if request.ContentLength == 0 {
return ErrRequestBodyEmpty
}
if request.ContentLength > maxBodySize {
return ErrRequestBodyTooLarge
}
return nil
}
func checkRequestAsPost(request *http.Request, maxBodySize int64) *APIError {
if err := checkContentLength(request, maxBodySize); err != nil {
return err
}
if request.Header.Get("Content-Type") != JSONMediaType {
return ErrWrongContentType
}
if request.Method != "POST" {
return ErrWrongRequestMethod
}
return nil
}
// ReadBody checks that a POST request is well-formed and reads its body.
func ReadBody(request *http.Request, maxBodySize int64) ([]byte, *APIError) {
if err := checkRequestAsPost(request, maxBodySize); err != nil {
return nil, err
}
body := make([]byte, request.ContentLength)
_, err := io.ReadFull(request.Body, body)
if err != nil {
return nil, ErrCouldNotReadBody
}
return body, nil
}
var zeroTime = time.Time{}
func checkBroadcast(bcast *Broadcast) (time.Time, *APIError) {
if len(bcast.Data) == 0 {
return zeroTime, ErrMissingData
}
expire, err := time.Parse(time.RFC3339, bcast.ExpireOn)
if err != nil {
return zeroTime, ErrInvalidExpiration
}
if expire.Before(time.Now()) {
return zeroTime, ErrPastExpiration
}
return expire, nil
}
type StoreForRequest func(w http.ResponseWriter, request *http.Request) (store.PendingStore, error)
// context holds the interfaces to delegate to serving requests
type context struct {
storeForRequest StoreForRequest
broker broker.BrokerSending
logger logger.Logger
}
func (ctx *context) getStore(w http.ResponseWriter, request *http.Request) (store.PendingStore, *APIError) {
sto, err := ctx.storeForRequest(w, request)
if err != nil {
apiErr, ok := err.(*APIError)
if ok {
return nil, apiErr
}
ctx.logger.Errorf("failed to get store: %v", err)
return nil, ErrUnknown
}
return sto, nil
}
type BroadcastHandler struct {
*context
}
func (h *BroadcastHandler) doBroadcast(sto store.PendingStore, bcast *Broadcast) *APIError {
expire, apiErr := checkBroadcast(bcast)
if apiErr != nil {
return apiErr
}
chanId, err := sto.GetInternalChannelId(bcast.Channel)
if err != nil {
switch err {
case store.ErrUnknownChannel:
return ErrUnknownChannel
default:
return ErrUnknown
}
}
err = sto.AppendToChannel(chanId, bcast.Data, expire)
if err != nil {
h.logger.Errorf("could not store notification: %v", err)
return ErrCouldNotStoreNotification
}
h.broker.Broadcast(chanId)
return nil
}
func (h *BroadcastHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
var apiErr *APIError
defer func() {
if apiErr != nil {
RespondError(writer, apiErr)
}
}()
body, apiErr := ReadBody(request, MaxRequestBodyBytes)
if apiErr != nil {
return
}
sto, apiErr := h.getStore(writer, request)
if apiErr != nil {
return
}
defer sto.Close()
broadcast := &Broadcast{}
err := json.Unmarshal(body, broadcast)
if err != nil {
apiErr = ErrMalformedJSONObject
return
}
apiErr = h.doBroadcast(sto, broadcast)
if apiErr != nil {
return
}
writer.Header().Set("Content-Type", "application/json")
fmt.Fprintf(writer, `{"ok":true}`)
}
// MakeHandlersMux makes a handler that dispatches for the various API endpoints.
func MakeHandlersMux(storeForRequest StoreForRequest, broker broker.BrokerSending, logger logger.Logger) *http.ServeMux {
ctx := &context{
storeForRequest: storeForRequest,
broker: broker,
logger: logger,
}
mux := http.NewServeMux()
mux.Handle("/broadcast", &BroadcastHandler{context: ctx})
return mux
}
ubuntu-push-0.2+14.04.20140411/server/api/middleware.go 0000644 0000153 0177776 00000002366 12322032412 022765 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package api
import (
"fmt"
"net/http"
"launchpad.net/ubuntu-push/logger"
)
// PanicTo500Handler wraps another handler such that panics are recovered
// and 500 reported.
func PanicTo500Handler(h http.Handler, logger logger.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
if err := recover(); err != nil {
logger.PanicStackf("serving http: %v", err)
// best effort
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
fmt.Fprintf(w, `{"error":"internal","message":"INTERNAL SERVER ERROR"}`)
}
}()
h.ServeHTTP(w, req)
})
}
ubuntu-push-0.2+14.04.20140411/server/api/handlers_test.go 0000644 0000153 0177776 00000033720 12322032412 023505 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/server/store"
helpers "launchpad.net/ubuntu-push/testing"
)
func TestHandlers(t *testing.T) { TestingT(t) }
type handlersSuite struct {
messageEndpoint string
json string
client *http.Client
c *C
testlog *helpers.TestLogger
}
var _ = Suite(&handlersSuite{})
func (s *handlersSuite) SetUpTest(c *C) {
s.client = &http.Client{}
s.testlog = helpers.NewTestLogger(c, "error")
}
func (s *handlersSuite) TestAPIError(c *C) {
var apiErr error = &APIError{400, invalidRequest, "Message"}
c.Check(apiErr.Error(), Equals, "api invalid-request: Message")
wire, err := json.Marshal(apiErr)
c.Assert(err, IsNil)
c.Check(string(wire), Equals, `{"error":"invalid-request","message":"Message"}`)
}
func (s *handlersSuite) TestReadBodyReadError(c *C) {
r := bytes.NewReader([]byte{}) // eof too early
req, err := http.NewRequest("POST", "", r)
c.Assert(err, IsNil)
req.Header.Set("Content-Type", "application/json")
req.ContentLength = 1000
_, err = ReadBody(req, 2000)
c.Check(err, Equals, ErrCouldNotReadBody)
}
func (s *handlersSuite) TestReadBodyTooBig(c *C) {
r := bytes.NewReader([]byte{}) // not read
req, err := http.NewRequest("POST", "", r)
c.Assert(err, IsNil)
req.Header.Set("Content-Type", "application/json")
req.ContentLength = 3000
_, err = ReadBody(req, 2000)
c.Check(err, Equals, ErrRequestBodyTooLarge)
}
func (s *handlersSuite) TestGetStore(c *C) {
ctx := &context{storeForRequest: func(w http.ResponseWriter, r *http.Request) (store.PendingStore, error) {
return nil, ErrStoreUnavailable
}}
sto, apiErr := ctx.getStore(nil, nil)
c.Check(sto, IsNil)
c.Check(apiErr, Equals, ErrStoreUnavailable)
ctx = &context{storeForRequest: func(w http.ResponseWriter, r *http.Request) (store.PendingStore, error) {
return nil, errors.New("something else")
}, logger: s.testlog}
sto, apiErr = ctx.getStore(nil, nil)
c.Check(sto, IsNil)
c.Check(apiErr, Equals, ErrUnknown)
c.Check(s.testlog.Captured(), Equals, "ERROR failed to get store: something else\n")
}
var future = time.Now().Add(4 * time.Hour).Format(time.RFC3339)
func (s *handlersSuite) TestCheckBroadcast(c *C) {
payload := json.RawMessage(`{"foo":"bar"}`)
broadcast := &Broadcast{
Channel: "system",
ExpireOn: future,
Data: payload,
}
expire, err := checkBroadcast(broadcast)
c.Check(err, IsNil)
c.Check(expire.Format(time.RFC3339), Equals, future)
broadcast = &Broadcast{
Channel: "system",
ExpireOn: future,
}
_, err = checkBroadcast(broadcast)
c.Check(err, Equals, ErrMissingData)
broadcast = &Broadcast{
Channel: "system",
ExpireOn: "12:00",
Data: payload,
}
_, err = checkBroadcast(broadcast)
c.Check(err, Equals, ErrInvalidExpiration)
broadcast = &Broadcast{
Channel: "system",
ExpireOn: time.Now().Add(-10 * time.Hour).Format(time.RFC3339),
Data: payload,
}
_, err = checkBroadcast(broadcast)
c.Check(err, Equals, ErrPastExpiration)
}
type checkBrokerSending struct {
store store.PendingStore
chanId store.InternalChannelId
err error
top int64
payloads []json.RawMessage
}
func (cbsend *checkBrokerSending) Broadcast(chanId store.InternalChannelId) {
top, payloads, err := cbsend.store.GetChannelSnapshot(chanId)
cbsend.err = err
cbsend.chanId = chanId
cbsend.top = top
cbsend.payloads = payloads
}
func (s *handlersSuite) TestDoBroadcast(c *C) {
sto := store.NewInMemoryPendingStore()
bsend := &checkBrokerSending{store: sto}
bh := &BroadcastHandler{&context{nil, bsend, nil}}
payload := json.RawMessage(`{"a": 1}`)
apiErr := bh.doBroadcast(sto, &Broadcast{
Channel: "system",
ExpireOn: future,
Data: payload,
})
c.Check(apiErr, IsNil)
c.Check(bsend.err, IsNil)
c.Check(bsend.chanId, Equals, store.SystemInternalChannelId)
c.Check(bsend.top, Equals, int64(1))
c.Check(bsend.payloads, DeepEquals, []json.RawMessage{payload})
}
func (s *handlersSuite) TestDoBroadcastUnknownChannel(c *C) {
sto := store.NewInMemoryPendingStore()
bh := &BroadcastHandler{}
apiErr := bh.doBroadcast(sto, &Broadcast{
Channel: "unknown",
ExpireOn: future,
Data: json.RawMessage(`{"a": 1}`),
})
c.Check(apiErr, Equals, ErrUnknownChannel)
}
type interceptInMemoryPendingStore struct {
*store.InMemoryPendingStore
intercept func(meth string, err error) error
}
func (isto *interceptInMemoryPendingStore) GetInternalChannelId(channel string) (store.InternalChannelId, error) {
chanId, err := isto.InMemoryPendingStore.GetInternalChannelId(channel)
return chanId, isto.intercept("GetInternalChannelId", err)
}
func (isto *interceptInMemoryPendingStore) AppendToChannel(chanId store.InternalChannelId, payload json.RawMessage, expiration time.Time) error {
err := isto.InMemoryPendingStore.AppendToChannel(chanId, payload, expiration)
return isto.intercept("AppendToChannel", err)
}
func (s *handlersSuite) TestDoBroadcastUnknownError(c *C) {
sto := &interceptInMemoryPendingStore{
store.NewInMemoryPendingStore(),
func(meth string, err error) error {
return errors.New("other")
},
}
bh := &BroadcastHandler{}
apiErr := bh.doBroadcast(sto, &Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"a": 1}`),
})
c.Check(apiErr, Equals, ErrUnknown)
}
func (s *handlersSuite) TestDoBroadcastCouldNotStoreNotification(c *C) {
sto := &interceptInMemoryPendingStore{
store.NewInMemoryPendingStore(),
func(meth string, err error) error {
if meth == "AppendToChannel" {
return errors.New("fail")
}
return err
},
}
ctx := &context{logger: s.testlog}
bh := &BroadcastHandler{ctx}
apiErr := bh.doBroadcast(sto, &Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"a": 1}`),
})
c.Check(apiErr, Equals, ErrCouldNotStoreNotification)
c.Check(s.testlog.Captured(), Equals, "ERROR could not store notification: fail\n")
}
func newPostRequest(path string, message interface{}, server *httptest.Server) *http.Request {
packedMessage, err := json.Marshal(message)
if err != nil {
panic(err)
}
reader := bytes.NewReader(packedMessage)
url := server.URL + path
request, _ := http.NewRequest("POST", url, reader)
request.ContentLength = int64(reader.Len())
request.Header.Set("Content-Type", "application/json")
return request
}
func getResponseBody(response *http.Response) ([]byte, error) {
defer response.Body.Close()
return ioutil.ReadAll(response.Body)
}
func checkError(c *C, response *http.Response, apiErr *APIError) {
c.Check(response.StatusCode, Equals, apiErr.StatusCode)
c.Check(response.Header.Get("Content-Type"), Equals, "application/json")
error := &APIError{StatusCode: response.StatusCode}
body, err := getResponseBody(response)
c.Assert(err, IsNil)
err = json.Unmarshal(body, error)
c.Assert(err, IsNil)
c.Check(error, DeepEquals, apiErr)
}
type testBrokerSending struct {
chanId chan store.InternalChannelId
}
func (bsend testBrokerSending) Broadcast(chanId store.InternalChannelId) {
bsend.chanId <- chanId
}
func (s *handlersSuite) TestRespondsToBasicSystemBroadcast(c *C) {
sto := store.NewInMemoryPendingStore()
stoForReq := func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return sto, nil
}
bsend := testBrokerSending{make(chan store.InternalChannelId, 1)}
testServer := httptest.NewServer(MakeHandlersMux(stoForReq, bsend, nil))
defer testServer.Close()
payload := json.RawMessage(`{"foo":"bar"}`)
request := newPostRequest("/broadcast", &Broadcast{
Channel: "system",
ExpireOn: future,
Data: payload,
}, testServer)
response, err := s.client.Do(request)
c.Assert(err, IsNil)
c.Check(response.StatusCode, Equals, http.StatusOK)
c.Check(response.Header.Get("Content-Type"), Equals, "application/json")
body, err := getResponseBody(response)
c.Assert(err, IsNil)
dest := make(map[string]bool)
err = json.Unmarshal(body, &dest)
c.Assert(err, IsNil)
c.Check(dest, DeepEquals, map[string]bool{"ok": true})
top, _, err := sto.GetChannelSnapshot(store.SystemInternalChannelId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(1))
c.Check(<-bsend.chanId, Equals, store.SystemInternalChannelId)
}
func (s *handlersSuite) TestStoreUnavailable(c *C) {
stoForReq := func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return nil, ErrStoreUnavailable
}
testServer := httptest.NewServer(MakeHandlersMux(stoForReq, nil, nil))
defer testServer.Close()
payload := json.RawMessage(`{"foo":"bar"}`)
request := newPostRequest("/broadcast", &Broadcast{
Channel: "system",
ExpireOn: future,
Data: payload,
}, testServer)
response, err := s.client.Do(request)
c.Assert(err, IsNil)
checkError(c, response, ErrStoreUnavailable)
}
func (s *handlersSuite) TestFromBroadcastError(c *C) {
sto := store.NewInMemoryPendingStore()
stoForReq := func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return sto, nil
}
testServer := httptest.NewServer(MakeHandlersMux(stoForReq, nil, nil))
defer testServer.Close()
payload := json.RawMessage(`{"foo":"bar"}`)
request := newPostRequest("/broadcast", &Broadcast{
Channel: "unknown",
ExpireOn: future,
Data: payload,
}, testServer)
response, err := s.client.Do(request)
c.Assert(err, IsNil)
checkError(c, response, ErrUnknownChannel)
}
func (s *handlersSuite) TestMissingData(c *C) {
stoForReq := func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return store.NewInMemoryPendingStore(), nil
}
ctx := &context{stoForReq, nil, nil}
testServer := httptest.NewServer(&BroadcastHandler{ctx})
defer testServer.Close()
packedMessage := []byte(`{"channel": "system"}`)
reader := bytes.NewReader(packedMessage)
request, err := http.NewRequest("POST", testServer.URL, reader)
c.Assert(err, IsNil)
request.ContentLength = int64(len(packedMessage))
request.Header.Set("Content-Type", "application/json")
response, err := s.client.Do(request)
c.Assert(err, IsNil)
checkError(c, response, ErrMissingData)
}
func (s *handlersSuite) TestCannotBroadcastMalformedData(c *C) {
stoForReq := func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return store.NewInMemoryPendingStore(), nil
}
ctx := &context{stoForReq, nil, nil}
testServer := httptest.NewServer(&BroadcastHandler{ctx})
defer testServer.Close()
packedMessage := []byte("{some bogus-message: ")
reader := bytes.NewReader(packedMessage)
request, err := http.NewRequest("POST", testServer.URL, reader)
c.Assert(err, IsNil)
request.ContentLength = int64(len(packedMessage))
request.Header.Set("Content-Type", "application/json")
response, err := s.client.Do(request)
c.Assert(err, IsNil)
checkError(c, response, ErrMalformedJSONObject)
}
func (s *handlersSuite) TestCannotBroadcastTooBigMessages(c *C) {
testServer := httptest.NewServer(&BroadcastHandler{})
defer testServer.Close()
bigString := strings.Repeat("a", MaxRequestBodyBytes)
dataString := fmt.Sprintf(`"%v"`, bigString)
request := newPostRequest("/", &Broadcast{
Channel: "some-channel",
ExpireOn: future,
Data: json.RawMessage([]byte(dataString)),
}, testServer)
response, err := s.client.Do(request)
c.Assert(err, IsNil)
checkError(c, response, ErrRequestBodyTooLarge)
}
func (s *handlersSuite) TestCannotBroadcastWithoutContentLength(c *C) {
testServer := httptest.NewServer(&BroadcastHandler{})
defer testServer.Close()
dataString := `{"foo":"bar"}`
request := newPostRequest("/", &Broadcast{
Channel: "some-channel",
ExpireOn: future,
Data: json.RawMessage([]byte(dataString)),
}, testServer)
request.ContentLength = -1
response, err := s.client.Do(request)
c.Assert(err, IsNil)
checkError(c, response, ErrNoContentLengthProvided)
}
func (s *handlersSuite) TestCannotBroadcastEmptyMessages(c *C) {
testServer := httptest.NewServer(&BroadcastHandler{})
defer testServer.Close()
packedMessage := make([]byte, 0)
reader := bytes.NewReader(packedMessage)
request, err := http.NewRequest("POST", testServer.URL, reader)
c.Assert(err, IsNil)
request.ContentLength = int64(len(packedMessage))
request.Header.Set("Content-Type", "application/json")
response, err := s.client.Do(request)
c.Assert(err, IsNil)
checkError(c, response, ErrRequestBodyEmpty)
}
func (s *handlersSuite) TestCannotBroadcastNonJSONMessages(c *C) {
testServer := httptest.NewServer(&BroadcastHandler{})
defer testServer.Close()
dataString := `{"foo":"bar"}`
request := newPostRequest("/", &Broadcast{
Channel: "some-channel",
ExpireOn: future,
Data: json.RawMessage([]byte(dataString)),
}, testServer)
request.Header.Set("Content-Type", "text/plain")
response, err := s.client.Do(request)
c.Assert(err, IsNil)
checkError(c, response, ErrWrongContentType)
}
func (s *handlersSuite) TestCannotBroadcastNonPostMessages(c *C) {
testServer := httptest.NewServer(&BroadcastHandler{})
defer testServer.Close()
dataString := `{"foo":"bar"}`
packedMessage, err := json.Marshal(&Broadcast{
Channel: "some-channel",
ExpireOn: future,
Data: json.RawMessage([]byte(dataString)),
})
s.c.Assert(err, IsNil)
reader := bytes.NewReader(packedMessage)
request, err := http.NewRequest("GET", testServer.URL, reader)
c.Assert(err, IsNil)
request.ContentLength = int64(len(packedMessage))
request.Header.Set("Content-Type", "application/json")
response, err := s.client.Do(request)
c.Assert(err, IsNil)
checkError(c, response, ErrWrongRequestMethod)
}
ubuntu-push-0.2+14.04.20140411/server/bootlog_test.go 0000644 0000153 0177776 00000002371 12322032412 022577 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package server
import (
"net"
"testing"
. "launchpad.net/gocheck"
helpers "launchpad.net/ubuntu-push/testing"
)
func TestRunners(t *testing.T) { TestingT(t) }
type bootlogSuite struct{}
var _ = Suite(&bootlogSuite{})
func (s *bootlogSuite) TestBootLogListener(c *C) {
prevBootLogger := BootLogger
testlog := helpers.NewTestLogger(c, "info")
BootLogger = testlog
defer func() {
BootLogger = prevBootLogger
}()
lst, err := net.Listen("tcp", "127.0.0.1:0")
c.Assert(err, IsNil)
defer lst.Close()
BootLogListener("client", lst)
c.Check(testlog.Captured(), Matches, "INFO listening for client on "+lst.Addr().String()+"\n")
}
ubuntu-push-0.2+14.04.20140411/server/config_test.go 0000644 0000153 0177776 00000004252 12322032412 022377 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package server
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/config"
)
type configSuite struct{}
var _ = Suite(&configSuite{})
func (s *configSuite) TestDevicesParsedConfig(c *C) {
buf := bytes.NewBufferString(`{
"ping_interval": "5m",
"exchange_timeout": "10s",
"session_queue_size": 10,
"broker_queue_size": 100,
"addr": "127.0.0.1:9999",
"key_pem_file": "key.key",
"cert_pem_file": "cert.cert"
}`)
cfg := &DevicesParsedConfig{}
err := config.ReadConfig(buf, cfg)
c.Assert(err, IsNil)
c.Check(cfg.PingInterval(), Equals, 5*time.Minute)
c.Check(cfg.ExchangeTimeout(), Equals, 10*time.Second)
c.Check(cfg.BrokerQueueSize(), Equals, uint(100))
c.Check(cfg.SessionQueueSize(), Equals, uint(10))
c.Check(cfg.Addr(), Equals, "127.0.0.1:9999")
}
func (s *configSuite) TestDevicesParsedConfigLoadFinish(c *C) {
tmpDir := c.MkDir()
cfg := &DevicesParsedConfig{
ParsedKeyPEMFile: "key.key",
ParsedCertPEMFile: "cert.cert",
}
err := cfg.FinishLoad(tmpDir)
c.Check(err, ErrorMatches, "reading key_pem_file:.*no such file.*")
err = ioutil.WriteFile(filepath.Join(tmpDir, "key.key"), []byte("KeY"), os.ModePerm)
c.Assert(err, IsNil)
err = cfg.FinishLoad(tmpDir)
c.Check(err, ErrorMatches, "reading cert_pem_file:.*no such file.*")
err = ioutil.WriteFile(filepath.Join(tmpDir, "cert.cert"), []byte("CeRt"), os.ModePerm)
c.Assert(err, IsNil)
err = cfg.FinishLoad(tmpDir)
c.Assert(err, IsNil)
c.Check(string(cfg.KeyPEMBlock()), Equals, "KeY")
c.Check(string(cfg.CertPEMBlock()), Equals, "CeRt")
}
ubuntu-push-0.2+14.04.20140411/server/runner_http.go 0000644 0000153 0177776 00000003422 12322032412 022441 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package server
import (
"net"
"net/http"
"launchpad.net/ubuntu-push/config"
)
// A HTTPServeParsedConfig holds and can be used to parse the HTTP server config.
type HTTPServeParsedConfig struct {
ParsedHTTPAddr config.ConfigHostPort `json:"http_addr"`
ParsedHTTPReadTimeout config.ConfigTimeDuration `json:"http_read_timeout"`
ParsedHTTPWriteTimeout config.ConfigTimeDuration `json:"http_write_timeout"`
}
// HTTPServeRunner returns a function to serve HTTP requests.
// If httpLst is not nil it will be used as the underlying listener.
func HTTPServeRunner(httpLst net.Listener, h http.Handler, parsedCfg *HTTPServeParsedConfig) func() {
if httpLst == nil {
var err error
httpLst, err = net.Listen("tcp", parsedCfg.ParsedHTTPAddr.HostPort())
if err != nil {
BootLogFatalf("start http listening: %v", err)
}
}
BootLogListener("http", httpLst)
srv := &http.Server{
Handler: h,
ReadTimeout: parsedCfg.ParsedHTTPReadTimeout.TimeDuration(),
WriteTimeout: parsedCfg.ParsedHTTPWriteTimeout.TimeDuration(),
}
return func() {
err := srv.Serve(httpLst)
if err != nil {
BootLogFatalf("accepting http connections: %v", err)
}
}
}
ubuntu-push-0.2+14.04.20140411/server/listener/ 0000755 0000153 0177776 00000000000 12322032700 021366 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/listener/listener_test.go 0000644 0000153 0177776 00000014570 12322032412 024610 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package listener
import (
"crypto/tls"
"crypto/x509"
"net"
"syscall"
"testing"
"time"
. "launchpad.net/gocheck"
helpers "launchpad.net/ubuntu-push/testing"
)
func TestListener(t *testing.T) { TestingT(t) }
type listenerSuite struct {
testlog *helpers.TestLogger
}
var _ = Suite(&listenerSuite{})
const NofileMax = 500
func (s *listenerSuite) SetUpSuite(*C) {
// make it easier to get a too many open files error
var nofileLimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &nofileLimit)
if err != nil {
panic(err)
}
nofileLimit.Cur = NofileMax
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &nofileLimit)
if err != nil {
panic(err)
}
}
func (s *listenerSuite) SetUpTest(c *C) {
s.testlog = helpers.NewTestLogger(c, "error")
}
type testDevListenerCfg struct {
addr string
}
func (cfg *testDevListenerCfg) Addr() string {
return cfg.addr
}
func (cfg *testDevListenerCfg) KeyPEMBlock() []byte {
return helpers.TestKeyPEMBlock
}
func (cfg *testDevListenerCfg) CertPEMBlock() []byte {
return helpers.TestCertPEMBlock
}
func (s *listenerSuite) TestDeviceListen(c *C) {
lst, err := DeviceListen(nil, &testDevListenerCfg{"127.0.0.1:0"})
c.Check(err, IsNil)
defer lst.Close()
c.Check(lst.Addr().String(), Matches, `127.0.0.1:\d{5}`)
}
func (s *listenerSuite) TestDeviceListenError(c *C) {
// assume tests are not running as root
_, err := DeviceListen(nil, &testDevListenerCfg{"127.0.0.1:99"})
c.Check(err, ErrorMatches, ".*permission denied.*")
}
type testNetError struct {
temp bool
}
func (tne *testNetError) Error() string {
return "test net error"
}
func (tne *testNetError) Temporary() bool {
return tne.temp
}
func (tne *testNetError) Timeout() bool {
return false
}
var _ net.Error = &testNetError{} // sanity check
func (s *listenerSuite) TestHandleTemporary(c *C) {
c.Check(handleTemporary(&testNetError{true}), Equals, true)
c.Check(handleTemporary(&testNetError{false}), Equals, false)
}
func testSession(conn net.Conn) error {
defer conn.Close()
conn.SetDeadline(time.Now().Add(2 * time.Second))
var buf [1]byte
_, err := conn.Read(buf[:])
if err != nil {
return err
}
_, err = conn.Write(buf[:])
return err
}
func testTlsDial(c *C, addr string) (net.Conn, error) {
cp := x509.NewCertPool()
ok := cp.AppendCertsFromPEM((&testDevListenerCfg{}).CertPEMBlock())
c.Assert(ok, Equals, true)
return tls.Dial("tcp", addr, &tls.Config{RootCAs: cp})
}
func testWriteByte(c *C, conn net.Conn, toWrite uint32) {
conn.SetDeadline(time.Now().Add(2 * time.Second))
_, err := conn.Write([]byte{byte(toWrite)})
c.Assert(err, IsNil)
}
func testReadByte(c *C, conn net.Conn, expected uint32) {
var buf [1]byte
_, err := conn.Read(buf[:])
c.Check(err, IsNil)
c.Check(buf[0], Equals, byte(expected))
}
func (s *listenerSuite) TestDeviceAcceptLoop(c *C) {
lst, err := DeviceListen(nil, &testDevListenerCfg{"127.0.0.1:0"})
c.Check(err, IsNil)
defer lst.Close()
errCh := make(chan error)
go func() {
errCh <- lst.AcceptLoop(testSession, s.testlog)
}()
listenerAddr := lst.Addr().String()
conn1, err := testTlsDial(c, listenerAddr)
c.Assert(err, IsNil)
defer conn1.Close()
testWriteByte(c, conn1, '1')
conn2, err := testTlsDial(c, listenerAddr)
c.Assert(err, IsNil)
defer conn2.Close()
testWriteByte(c, conn2, '2')
testReadByte(c, conn1, '1')
testReadByte(c, conn2, '2')
lst.Close()
c.Check(<-errCh, ErrorMatches, ".*use of closed.*")
c.Check(s.testlog.Captured(), Equals, "")
}
func (s *listenerSuite) TestDeviceAcceptLoopTemporaryError(c *C) {
// ENFILE is not the temp network error we want to handle this way
// but is relatively easy to generate in a controlled way
var err error
lst, err := DeviceListen(nil, &testDevListenerCfg{"127.0.0.1:0"})
c.Check(err, IsNil)
defer lst.Close()
errCh := make(chan error)
go func() {
errCh <- lst.AcceptLoop(testSession, s.testlog)
}()
listenerAddr := lst.Addr().String()
conns := make([]net.Conn, 0, NofileMax)
for i := 0; i < NofileMax; i++ {
var conn1 net.Conn
conn1, err = net.Dial("tcp", listenerAddr)
if err != nil {
break
}
defer conn1.Close()
conns = append(conns, conn1)
}
c.Assert(err, ErrorMatches, "*.too many open.*")
for _, conn := range conns {
conn.Close()
}
conn2, err := testTlsDial(c, listenerAddr)
c.Assert(err, IsNil)
defer conn2.Close()
testWriteByte(c, conn2, '2')
testReadByte(c, conn2, '2')
lst.Close()
c.Check(<-errCh, ErrorMatches, ".*use of closed.*")
c.Check(s.testlog.Captured(), Matches, ".*device listener:.*accept.*too many open.*-- retrying\n")
}
func (s *listenerSuite) TestDeviceAcceptLoopPanic(c *C) {
lst, err := DeviceListen(nil, &testDevListenerCfg{"127.0.0.1:0"})
c.Check(err, IsNil)
defer lst.Close()
errCh := make(chan error)
go func() {
errCh <- lst.AcceptLoop(func(conn net.Conn) error {
defer conn.Close()
panic("session crash")
}, s.testlog)
}()
listenerAddr := lst.Addr().String()
_, err = testTlsDial(c, listenerAddr)
c.Assert(err, Not(IsNil))
lst.Close()
c.Check(<-errCh, ErrorMatches, ".*use of closed.*")
c.Check(s.testlog.Captured(), Matches, "(?s)ERROR\\(PANIC\\) terminating device connection on: session crash:.*AcceptLoop.*")
}
func (s *listenerSuite) TestForeignListener(c *C) {
foreignLst, err := net.Listen("tcp", "127.0.0.1:0")
c.Check(err, IsNil)
lst, err := DeviceListen(foreignLst, &testDevListenerCfg{"127.0.0.1:0"})
c.Check(err, IsNil)
defer lst.Close()
errCh := make(chan error)
go func() {
errCh <- lst.AcceptLoop(testSession, s.testlog)
}()
listenerAddr := lst.Addr().String()
c.Check(listenerAddr, Equals, foreignLst.Addr().String())
conn1, err := testTlsDial(c, listenerAddr)
c.Assert(err, IsNil)
defer conn1.Close()
testWriteByte(c, conn1, '1')
testReadByte(c, conn1, '1')
lst.Close()
c.Check(<-errCh, ErrorMatches, ".*use of closed.*")
c.Check(s.testlog.Captured(), Equals, "")
}
ubuntu-push-0.2+14.04.20140411/server/listener/listener.go 0000644 0000153 0177776 00000005243 12322032412 023546 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package listener has code to listen for device connections
// and setup sessions for them.
package listener
import (
"crypto/tls"
"net"
"time"
"launchpad.net/ubuntu-push/logger"
)
// A DeviceListenerConfig offers the DeviceListener configuration.
type DeviceListenerConfig interface {
// Addr to listen on.
Addr() string
// TLS key
KeyPEMBlock() []byte
// TLS cert
CertPEMBlock() []byte
}
// DeviceListener listens and setup sessions from device connections.
type DeviceListener struct {
net.Listener
}
// DeviceListen creates a DeviceListener for device connections based
// on config. If lst is not nil DeviceListen just wraps it with a TLS
// layer instead of starting creating a new listener.
func DeviceListen(lst net.Listener, cfg DeviceListenerConfig) (*DeviceListener, error) {
if lst == nil {
var err error
lst, err = net.Listen("tcp", cfg.Addr())
if err != nil {
return nil, err
}
}
cert, err := tls.X509KeyPair(cfg.CertPEMBlock(), cfg.KeyPEMBlock())
if err != nil {
return nil, err
}
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
SessionTicketsDisabled: true,
}
return &DeviceListener{tls.NewListener(lst, tlsCfg)}, err
}
// handleTemporary checks and handles if the error is just a temporary network
// error.
func handleTemporary(err error) bool {
if netError, isNetError := err.(net.Error); isNetError {
if netError.Temporary() {
// wait, xxx exponential backoff?
time.Sleep(100 * time.Millisecond)
return true
}
}
return false
}
// AcceptLoop accepts connections and starts sessions for them.
func (dl *DeviceListener) AcceptLoop(session func(net.Conn) error, logger logger.Logger) error {
for {
// xxx enforce a connection limit
conn, err := dl.Listener.Accept()
if err != nil {
if handleTemporary(err) {
logger.Errorf("device listener: %s -- retrying", err)
continue
}
return err
}
go func() {
defer func() {
if err := recover(); err != nil {
logger.PanicStackf("terminating device connection on: %v", err)
}
}()
session(conn)
}()
}
}
ubuntu-push-0.2+14.04.20140411/server/acceptance/ 0000755 0000153 0177776 00000000000 12322032700 021627 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/acceptance/acceptanceclient.go 0000644 0000153 0177776 00000007321 12322032421 025446 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package acceptance contains the acceptance client.
package acceptance
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"net"
"time"
"launchpad.net/ubuntu-push/protocol"
)
var wireVersionBytes = []byte{protocol.ProtocolWireVersion}
// ClienSession holds a client<->server session and its configuration.
type ClientSession struct {
// configuration
DeviceId string
Model string
ImageChannel string
ServerAddr string
ExchangeTimeout time.Duration
CertPEMBlock []byte
ReportPings bool
Levels map[string]int64
Insecure bool // don't verify certs
Prefix string // prefix for events
// connection
Connection net.Conn
}
// Dial connects to a server using the configuration in the ClientSession
// and sets up the connection.
func (sess *ClientSession) Dial() error {
conn, err := net.DialTimeout("tcp", sess.ServerAddr, sess.ExchangeTimeout)
if err != nil {
return err
}
tlsConfig := &tls.Config{}
if sess.CertPEMBlock != nil {
cp := x509.NewCertPool()
ok := cp.AppendCertsFromPEM(sess.CertPEMBlock)
if !ok {
return errors.New("dial: could not parse certificate")
}
tlsConfig.RootCAs = cp
}
tlsConfig.InsecureSkipVerify = sess.Insecure
sess.Connection = tls.Client(conn, tlsConfig)
return nil
}
type serverMsg struct {
Type string `json:"T"`
protocol.BroadcastMsg
protocol.NotificationsMsg
}
// Run the session with the server, emits a stream of events.
func (sess *ClientSession) Run(events chan<- string) error {
conn := sess.Connection
defer conn.Close()
conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
_, err := conn.Write(wireVersionBytes)
if err != nil {
return err
}
proto := protocol.NewProtocol0(conn)
err = proto.WriteMessage(protocol.ConnectMsg{
Type: "connect",
DeviceId: sess.DeviceId,
Levels: sess.Levels,
Info: map[string]interface{}{
"device": sess.Model,
"channel": sess.ImageChannel,
},
})
if err != nil {
return err
}
var connAck protocol.ConnAckMsg
err = proto.ReadMessage(&connAck)
if err != nil {
return err
}
pingInterval, err := time.ParseDuration(connAck.Params.PingInterval)
if err != nil {
return err
}
events <- fmt.Sprintf("%sconnected %v", sess.Prefix, conn.LocalAddr())
var recv serverMsg
for {
deadAfter := pingInterval + sess.ExchangeTimeout
conn.SetDeadline(time.Now().Add(deadAfter))
err = proto.ReadMessage(&recv)
if err != nil {
return err
}
switch recv.Type {
case "ping":
conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
err := proto.WriteMessage(protocol.PingPongMsg{Type: "pong"})
if err != nil {
return err
}
if sess.ReportPings {
events <- sess.Prefix + "ping"
}
case "broadcast":
conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
err := proto.WriteMessage(protocol.PingPongMsg{Type: "ack"})
if err != nil {
return err
}
pack, err := json.Marshal(recv.Payloads)
if err != nil {
return err
}
events <- fmt.Sprintf("%sbroadcast chan:%v app:%v topLevel:%d payloads:%s", sess.Prefix, recv.ChanId, recv.AppId, recv.TopLevel, pack)
}
}
return nil
}
ubuntu-push-0.2+14.04.20140411/server/acceptance/acceptance.sh 0000755 0000153 0177776 00000000553 12322032412 024257 0 ustar pbuser nogroup 0000000 0000000 # run acceptance tests, expects properly setup GOPATH and deps
# can set extra build params like -race with BUILD_FLAGS envvar
# can set server pkg name with SERVER_PKG
set -ex
go test $BUILD_FLAGS -i launchpad.net/ubuntu-push/server/acceptance
go build $BUILD_FLAGS -o testserver launchpad.net/ubuntu-push/server/dev
go test $BUILD_FLAGS -server ./testserver $*
ubuntu-push-0.2+14.04.20140411/server/acceptance/suites/ 0000755 0000153 0177776 00000000000 12322032700 023143 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/acceptance/suites/helpers.go 0000644 0000153 0177776 00000010550 12322032412 025135 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package suites
import (
"bufio"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
. "launchpad.net/gocheck"
helpers "launchpad.net/ubuntu-push/testing"
)
// FillConfig fills cfg from values.
func FillConfig(cfg, values map[string]interface{}) {
for k, v := range values {
cfg[k] = v
}
}
// FillServerConfig fills cfg with default server values and "addr": addr.
func FillServerConfig(cfg map[string]interface{}, addr string) {
FillConfig(cfg, map[string]interface{}{
"exchange_timeout": "0.1s",
"ping_interval": "0.5s",
"session_queue_size": 10,
"broker_queue_size": 100,
"addr": addr,
"key_pem_file": helpers.SourceRelative("../ssl/testing.key"),
"cert_pem_file": helpers.SourceRelative("../ssl/testing.cert"),
})
}
// FillHttpServerConfig fills cfg with default http server values and
// "http_addr": httpAddr.
func FillHTTPServerConfig(cfg map[string]interface{}, httpAddr string) {
FillConfig(cfg, map[string]interface{}{
"http_addr": httpAddr,
"http_read_timeout": "1s",
"http_write_timeout": "1s",
})
}
// WriteConfig writes out a config and returns the written filepath.
func WriteConfig(c *C, dir, filename string, cfg map[string]interface{}) string {
cfgFpath := filepath.Join(dir, filename)
cfgJson, err := json.Marshal(cfg)
if err != nil {
c.Fatal(err)
}
err = ioutil.WriteFile(cfgFpath, cfgJson, os.ModePerm)
if err != nil {
c.Fatal(err)
}
return cfgFpath
}
var rxLineInfo = regexp.MustCompile("^.*? ([[:alpha:]].*)\n")
// RunAndObserve runs cmdName and returns a channel that will receive
// cmdName stderr logging and a function to kill the process.
func RunAndObserve(c *C, cmdName string, arg ...string) (<-chan string, func(os.Signal)) {
cmd := exec.Command(cmdName, arg...)
stderr, err := cmd.StderrPipe()
if err != nil {
c.Fatal(err)
}
err = cmd.Start()
if err != nil {
c.Fatal(err)
}
bufErr := bufio.NewReaderSize(stderr, 5000)
getLineInfo := func(full bool) (string, error) {
for {
line, err := bufErr.ReadString('\n')
if err != nil {
return "", err
}
if full {
return strings.TrimRight(line, "\n"), nil
}
extracted := rxLineInfo.FindStringSubmatch(line)
if extracted == nil {
return "", fmt.Errorf("unexpected line: %#v", line)
}
info := extracted[1]
return info, nil
}
}
logs := make(chan string, 10)
go func() {
panicked := false
for {
info, err := getLineInfo(panicked)
if err != nil {
logs <- fmt.Sprintf("%s capture: %v", cmdName, err)
close(logs)
return
}
if panicked || strings.HasPrefix(info, "ERROR(PANIC") {
panicked = true
c.Log(info)
continue
}
logs <- info
}
}()
return logs, func(sig os.Signal) { cmd.Process.Signal(sig) }
}
const (
DevListeningOnPat = "INFO listening for devices on "
HTTPListeningOnPat = "INFO listening for http on "
debugPrefix = "DEBUG "
)
// ExtractListeningAddr goes over logs ignoring DEBUG lines
// until a line starting with pat and returns the rest of that line.
func ExtractListeningAddr(c *C, logs <-chan string, pat string) string {
for line := range logs {
if strings.HasPrefix(line, debugPrefix) {
continue
}
if !strings.HasPrefix(line, pat) {
c.Fatalf("matching %v: %v", pat, line)
}
return line[len(pat):]
}
panic(fmt.Errorf("logs closed unexpectedly marching %v", pat))
}
// NextEvent receives an event from given string channel with a 5s timeout,
// or from a channel for errors.
func NextEvent(events <-chan string, errCh <-chan error) string {
select {
case <-time.After(5 * time.Second):
panic("too long stuck waiting for next event")
case err := <-errCh:
return err.Error() // will fail comparison typically
case evStr := <-events:
return evStr
}
}
ubuntu-push-0.2+14.04.20140411/server/acceptance/suites/broadcast.go 0000644 0000153 0177776 00000022155 12322032421 025441 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package suites
import (
"encoding/json"
"fmt"
"strings"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/client/gethosts"
"launchpad.net/ubuntu-push/protocol"
"launchpad.net/ubuntu-push/server/api"
)
// BroadCastAcceptanceSuite has tests about broadcast.
type BroadcastAcceptanceSuite struct {
AcceptanceSuite
}
// Long after the end of the tests.
var future = time.Now().Add(9 * time.Hour).Format(time.RFC3339)
func (s *BroadcastAcceptanceSuite) TestBroadcastToConnected(c *C) {
events, errCh, stop := s.StartClient(c, "DEVB", nil)
got, err := s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 42}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
c.Check(NextEvent(events, errCh), Equals, `broadcast chan:0 app: topLevel:1 payloads:[{"img1/m1":42}]`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *BroadcastAcceptanceSuite) TestBroadcastToConnectedChannelFilter(c *C) {
events, errCh, stop := s.StartClient(c, "DEVB", nil)
got, err := s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m2": 10}`),
})
c.Assert(err, IsNil)
got, err = s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 20}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
c.Check(NextEvent(events, errCh), Equals, `broadcast chan:0 app: topLevel:2 payloads:[{"img1/m1":20}]`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *BroadcastAcceptanceSuite) TestBroadcastPending(c *C) {
// send broadcast that will be pending
got, err := s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 1}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
events, errCh, stop := s.StartClient(c, "DEVB", nil)
// gettting pending on connect
c.Check(NextEvent(events, errCh), Equals, `broadcast chan:0 app: topLevel:1 payloads:[{"img1/m1":1}]`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *BroadcastAcceptanceSuite) TestBroadcasLargeNeedsSplitting(c *C) {
// send bunch of broadcasts that will be pending
payloadFmt := fmt.Sprintf(`{"img1/m1":%%d,"bloat":"%s"}`, strings.Repeat("x", 1024*2))
for i := 0; i < 32; i++ {
got, err := s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(fmt.Sprintf(payloadFmt, i)),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
}
events, errCh, stop := s.StartClient(c, "DEVC", nil)
// gettting pending on connect
c.Check(NextEvent(events, errCh), Matches, `broadcast chan:0 app: topLevel:30 payloads:\[{"img1/m1":0,.*`)
c.Check(NextEvent(events, errCh), Matches, `broadcast chan:0 app: topLevel:32 payloads:\[.*`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *BroadcastAcceptanceSuite) TestBroadcastDistribution2(c *C) {
// start 1st client
events1, errCh1, stop1 := s.StartClient(c, "DEV1", nil)
// start 2nd client
events2, errCh2, stop2 := s.StartClient(c, "DEV2", nil)
// broadcast
got, err := s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 42}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
c.Check(NextEvent(events1, errCh1), Equals, `broadcast chan:0 app: topLevel:1 payloads:[{"img1/m1":42}]`)
c.Check(NextEvent(events2, errCh2), Equals, `broadcast chan:0 app: topLevel:1 payloads:[{"img1/m1":42}]`)
stop1()
stop2()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh1), Equals, 0)
c.Check(len(errCh2), Equals, 0)
}
func (s *BroadcastAcceptanceSuite) TestBroadcastFilterByLevel(c *C) {
events, errCh, stop := s.StartClient(c, "DEVD", nil)
got, err := s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 1}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
c.Check(NextEvent(events, errCh), Equals, `broadcast chan:0 app: topLevel:1 payloads:[{"img1/m1":1}]`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
// another broadcast
got, err = s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 2}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
// reconnect, provide levels, get only later notification
events, errCh, stop = s.StartClient(c, "DEVD", map[string]int64{
protocol.SystemChannelId: 1,
})
c.Check(NextEvent(events, errCh), Equals, `broadcast chan:0 app: topLevel:2 payloads:[{"img1/m1":2}]`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *BroadcastAcceptanceSuite) TestBroadcastTooAhead(c *C) {
// send broadcasts that will be pending
got, err := s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 1}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
got, err = s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 2}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
events, errCh, stop := s.StartClient(c, "DEVB", map[string]int64{
protocol.SystemChannelId: 10,
})
// gettting last one pending on connect
c.Check(NextEvent(events, errCh), Equals, `broadcast chan:0 app: topLevel:2 payloads:[{"img1/m1":2}]`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *BroadcastAcceptanceSuite) TestBroadcastTooAheadOnEmpty(c *C) {
// nothing there
events, errCh, stop := s.StartClient(c, "DEVB", map[string]int64{
protocol.SystemChannelId: 10,
})
// gettting empty pending on connect
c.Check(NextEvent(events, errCh), Equals, `broadcast chan:0 app: topLevel:0 payloads:null`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *BroadcastAcceptanceSuite) TestBroadcastWayBehind(c *C) {
// send broadcasts that will be pending
got, err := s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 1}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
got, err = s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 2}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
events, errCh, stop := s.StartClient(c, "DEVB", map[string]int64{
protocol.SystemChannelId: -10,
})
// gettting pending on connect
c.Check(NextEvent(events, errCh), Equals, `broadcast chan:0 app: topLevel:2 payloads:[{"img1/m1":1},{"img1/m1":2}]`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *BroadcastAcceptanceSuite) TestBroadcastExpiration(c *C) {
// send broadcast that will be pending, and one that will expire
got, err := s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 1}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
got, err = s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: time.Now().Add(1 * time.Second).Format(time.RFC3339),
Data: json.RawMessage(`{"img1/m1": 2}`),
})
c.Assert(err, IsNil)
c.Assert(got, Matches, ".*ok.*")
time.Sleep(2 * time.Second)
// second broadcast is expired
events, errCh, stop := s.StartClient(c, "DEVB", nil)
// gettting pending on connect
c.Check(NextEvent(events, errCh), Equals, `broadcast chan:0 app: topLevel:2 payloads:[{"img1/m1":1}]`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
// test /delivery-hosts
func (s *BroadcastAcceptanceSuite) TestGetHosts(c *C) {
gh := gethosts.New("", s.ServerAPIURL+"/delivery-hosts", 2*time.Second)
hosts, err := gh.Get()
c.Assert(err, IsNil)
c.Check(hosts, DeepEquals, []string{s.ServerAddr})
}
ubuntu-push-0.2+14.04.20140411/server/acceptance/suites/suite.go 0000644 0000153 0177776 00000012521 12322032412 024624 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package suites contains reusable acceptance test suites.
package suites
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"runtime"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/server/acceptance"
helpers "launchpad.net/ubuntu-push/testing"
)
// ServerHandle holds the information to attach a client to the test server.
type ServerHandle struct {
ServerAddr string
ServerHTTPAddr string
ServerEvents <-chan string
}
// Start a client.
func (h *ServerHandle) StartClient(c *C, devId string, levels map[string]int64) (events <-chan string, errorCh <-chan error, stop func()) {
errCh := make(chan error, 1)
cliEvents := make(chan string, 10)
sess := testClientSession(h.ServerAddr, devId, "m1", "img1", false)
sess.Levels = levels
err := sess.Dial()
c.Assert(err, IsNil)
clientShutdown := make(chan bool, 1) // abused as an atomic flag
intercept := func(ic *interceptingConn, op string, b []byte) (bool, int, error) {
// read after ack
if op == "read" && len(clientShutdown) > 0 {
// exit the sess.Run() goroutine, client will close
runtime.Goexit()
}
return false, 0, nil
}
sess.Connection = &interceptingConn{sess.Connection, 0, 0, intercept}
go func() {
errCh <- sess.Run(cliEvents)
}()
c.Assert(NextEvent(cliEvents, errCh), Matches, "connected .*")
c.Assert(NextEvent(h.ServerEvents, nil), Matches, ".*session.* connected .*")
c.Assert(NextEvent(h.ServerEvents, nil), Matches, ".*session.* registered "+devId)
return cliEvents, errCh, func() { clientShutdown <- true }
}
// AcceptanceSuite has the basic functionality of the acceptance suites.
type AcceptanceSuite struct {
// hook to start the server(s)
StartServer func(c *C, s *AcceptanceSuite, handle *ServerHandle)
// populated by StartServer
ServerHandle
ServerAPIURL string
// KillGroup should be populated by StartServer with functions
// to kill the server process
KillGroup map[string]func(os.Signal)
// hook to adjust requests
MassageRequest func(req *http.Request) *http.Request
// other state
httpClient *http.Client
}
// Start a new server for each test.
func (s *AcceptanceSuite) SetUpTest(c *C) {
s.KillGroup = make(map[string]func(os.Signal))
s.StartServer(c, s, &s.ServerHandle)
c.Assert(s.ServerHandle.ServerEvents, NotNil)
c.Assert(s.ServerHandle.ServerAddr, Not(Equals), "")
c.Assert(s.ServerAPIURL, Not(Equals), "")
s.httpClient = &http.Client{}
}
func (s *AcceptanceSuite) TearDownTest(c *C) {
for _, f := range s.KillGroup {
f(os.Kill)
}
}
// Post a API request.
func (s *AcceptanceSuite) PostRequest(path string, message interface{}) (string, error) {
packedMessage, err := json.Marshal(message)
if err != nil {
panic(err)
}
reader := bytes.NewReader(packedMessage)
url := s.ServerAPIURL + path
request, _ := http.NewRequest("POST", url, reader)
request.ContentLength = int64(reader.Len())
request.Header.Set("Content-Type", "application/json")
if s.MassageRequest != nil {
request = s.MassageRequest(request)
}
resp, err := s.httpClient.Do(request)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
return string(body), err
}
func testClientSession(addr string, deviceId, model, imageChannel string, reportPings bool) *acceptance.ClientSession {
certPEMBlock, err := ioutil.ReadFile(helpers.SourceRelative("../ssl/testing.cert"))
if err != nil {
panic(fmt.Sprintf("could not read ssl/testing.cert: %v", err))
}
return &acceptance.ClientSession{
ExchangeTimeout: 100 * time.Millisecond,
ServerAddr: addr,
CertPEMBlock: certPEMBlock,
DeviceId: deviceId,
Model: model,
ImageChannel: imageChannel,
ReportPings: reportPings,
}
}
// typically combined with -gocheck.vv or test selection
var logTraffic = flag.Bool("logTraffic", false, "log traffic")
type connInterceptor func(ic *interceptingConn, op string, b []byte) (bool, int, error)
type interceptingConn struct {
net.Conn
totalRead int
totalWritten int
intercept connInterceptor
}
func (ic *interceptingConn) Write(b []byte) (n int, err error) {
done := false
before := ic.totalWritten
if ic.intercept != nil {
done, n, err = ic.intercept(ic, "write", b)
}
if !done {
n, err = ic.Conn.Write(b)
}
ic.totalWritten += n
if *logTraffic {
fmt.Printf("W[%v]: %d %#v %v %d\n", ic.Conn.LocalAddr(), before, string(b[:n]), err, ic.totalWritten)
}
return
}
func (ic *interceptingConn) Read(b []byte) (n int, err error) {
done := false
before := ic.totalRead
if ic.intercept != nil {
done, n, err = ic.intercept(ic, "read", b)
}
if !done {
n, err = ic.Conn.Read(b)
}
ic.totalRead += n
if *logTraffic {
fmt.Printf("R[%v]: %d %#v %v %d\n", ic.Conn.LocalAddr(), before, string(b[:n]), err, ic.totalRead)
}
return
}
ubuntu-push-0.2+14.04.20140411/server/acceptance/suites/pingpong.go 0000644 0000153 0177776 00000006440 12322032421 025317 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package suites
import (
"runtime"
"strings"
"time"
. "launchpad.net/gocheck"
)
// PingPongAcceptanceSuite has tests about connectivity and ping-pong requests.
type PingPongAcceptanceSuite struct {
AcceptanceSuite
}
// Tests about connection, ping-pong, disconnection scenarios
func (s *PingPongAcceptanceSuite) TestConnectPingPing(c *C) {
errCh := make(chan error, 1)
events := make(chan string, 10)
sess := testClientSession(s.ServerAddr, "DEVA", "m1", "img1", true)
err := sess.Dial()
c.Assert(err, IsNil)
intercept := func(ic *interceptingConn, op string, b []byte) (bool, int, error) {
// would be 3rd ping read, based on logged traffic
if op == "read" && ic.totalRead >= 79 {
// exit the sess.Run() goroutine, client will close
runtime.Goexit()
}
return false, 0, nil
}
sess.Connection = &interceptingConn{sess.Connection, 0, 0, intercept}
go func() {
errCh <- sess.Run(events)
}()
connectCli := NextEvent(events, errCh)
connectSrv := NextEvent(s.ServerEvents, nil)
registeredSrv := NextEvent(s.ServerEvents, nil)
tconnect := time.Now()
c.Assert(connectSrv, Matches, ".*session.* connected .*")
c.Assert(registeredSrv, Matches, ".*session.* registered DEVA")
c.Assert(strings.HasSuffix(connectSrv, connectCli), Equals, true)
c.Assert(NextEvent(events, errCh), Equals, "ping")
elapsedOfPing := float64(time.Since(tconnect)) / float64(500*time.Millisecond)
c.Check(elapsedOfPing >= 1.0, Equals, true)
c.Check(elapsedOfPing < 1.05, Equals, true)
c.Assert(NextEvent(events, errCh), Equals, "ping")
c.Assert(NextEvent(s.ServerEvents, nil), Matches, ".*session.* ended with: EOF")
c.Check(len(errCh), Equals, 0)
}
func (s *PingPongAcceptanceSuite) TestConnectPingNeverPong(c *C) {
errCh := make(chan error, 1)
events := make(chan string, 10)
sess := testClientSession(s.ServerAddr, "DEVB", "m1", "img1", true)
err := sess.Dial()
c.Assert(err, IsNil)
intercept := func(ic *interceptingConn, op string, b []byte) (bool, int, error) {
// would be pong to 2nd ping, based on logged traffic
if op == "write" && ic.totalRead >= 67 {
time.Sleep(200 * time.Millisecond)
// exit the sess.Run() goroutine, client will close
runtime.Goexit()
}
return false, 0, nil
}
sess.Connection = &interceptingConn{sess.Connection, 0, 0, intercept}
go func() {
errCh <- sess.Run(events)
}()
c.Assert(NextEvent(events, errCh), Matches, "connected .*")
c.Assert(NextEvent(s.ServerEvents, nil), Matches, ".*session.* connected .*")
c.Assert(NextEvent(s.ServerEvents, nil), Matches, ".*session.* registered .*")
c.Assert(NextEvent(events, errCh), Equals, "ping")
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*timeout`)
c.Check(len(errCh), Equals, 0)
}
ubuntu-push-0.2+14.04.20140411/server/acceptance/cmd/ 0000755 0000153 0177776 00000000000 12322032700 022372 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/acceptance/cmd/acceptanceclient.go 0000644 0000153 0177776 00000005154 12322032421 026213 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// acceptanceclient command for playing.
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"launchpad.net/ubuntu-push/config"
"launchpad.net/ubuntu-push/server/acceptance"
)
var (
insecureFlag = flag.Bool("insecure", false, "disable checking of server certificate and hostname")
reportPingsFlag = flag.Bool("reportPings", true, "report each Ping from the server")
deviceModel = flag.String("model", "?", "device image model")
imageChannel = flag.String("imageChannel", "?", "image channel")
)
type configuration struct {
// session configuration
ExchangeTimeout config.ConfigTimeDuration `json:"exchange_timeout"`
// server connection config
Addr config.ConfigHostPort
CertPEMFile string `json:"cert_pem_file"`
}
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: acceptancclient [options] \n")
flag.PrintDefaults()
}
flag.Parse()
narg := flag.NArg()
switch {
case narg < 1:
log.Fatal("missing config file")
case narg < 2:
log.Fatal("missing device-id")
}
configFName := flag.Arg(0)
f, err := os.Open(configFName)
if err != nil {
log.Fatalf("reading config: %v", err)
}
cfg := &configuration{}
err = config.ReadConfig(f, cfg)
if err != nil {
log.Fatalf("reading config: %v", err)
}
session := &acceptance.ClientSession{
ExchangeTimeout: cfg.ExchangeTimeout.TimeDuration(),
ServerAddr: cfg.Addr.HostPort(),
DeviceId: flag.Arg(1),
// flags
Model: *deviceModel,
ImageChannel: *imageChannel,
ReportPings: *reportPingsFlag,
Insecure: *insecureFlag,
}
log.Printf("with: %#v", session)
if !*insecureFlag {
session.CertPEMBlock, err = config.LoadFile(cfg.CertPEMFile, filepath.Dir(configFName))
if err != nil {
log.Fatalf("reading CertPEMFile: %v", err)
}
}
err = session.Dial()
if err != nil {
log.Fatalln(err)
}
events := make(chan string, 5)
go func() {
for {
log.Println(<-events)
}
}()
err = session.Run(events)
if err != nil {
log.Fatalln(err)
}
}
ubuntu-push-0.2+14.04.20140411/server/acceptance/acceptance_test.go 0000644 0000153 0177776 00000004002 12322032421 025277 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package acceptance_test
import (
"flag"
"fmt"
"testing"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/server/acceptance/suites"
)
func TestAcceptance(t *testing.T) { TestingT(t) }
var serverCmd = flag.String("server", "", "server to test")
func testServerConfig(addr, httpAddr string) map[string]interface{} {
cfg := make(map[string]interface{})
suites.FillServerConfig(cfg, addr)
suites.FillHTTPServerConfig(cfg, httpAddr)
cfg["delivery_domain"] = "localhost"
return cfg
}
// Start a server.
func StartServer(c *C, s *suites.AcceptanceSuite, handle *suites.ServerHandle) {
if *serverCmd == "" {
c.Skip("executable server not specified")
}
tmpDir := c.MkDir()
cfg := testServerConfig("127.0.0.1:0", "127.0.0.1:0")
cfgFilename := suites.WriteConfig(c, tmpDir, "config.json", cfg)
logs, killServer := suites.RunAndObserve(c, *serverCmd, cfgFilename)
s.KillGroup["server"] = killServer
handle.ServerHTTPAddr = suites.ExtractListeningAddr(c, logs, suites.HTTPListeningOnPat)
s.ServerAPIURL = fmt.Sprintf("http://%s", handle.ServerHTTPAddr)
handle.ServerAddr = suites.ExtractListeningAddr(c, logs, suites.DevListeningOnPat)
handle.ServerEvents = logs
}
// ping pong/connectivity
var _ = Suite(&suites.PingPongAcceptanceSuite{suites.AcceptanceSuite{StartServer: StartServer}})
// broadcast
var _ = Suite(&suites.BroadcastAcceptanceSuite{suites.AcceptanceSuite{StartServer: StartServer}})
ubuntu-push-0.2+14.04.20140411/server/acceptance/ssl/ 0000755 0000153 0177776 00000000000 12322032700 022430 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/acceptance/ssl/testing.key 0000644 0000153 0177776 00000000761 12322032412 024623 0 ustar pbuser nogroup 0000000 0000000 -----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBAPw+niki17X2qALE2A2AzE1q5dvK9CI4OduRtT9IgbFLC6psqAT2
1NA+QbY17nWSSpyP65zkMkwKXrbDzstwLPkCAwEAAQJAKwXbIBULScP6QA6m8xam
wgWbkvN41GVWqPafPV32kPBvKwSc+M1e+JR7g3/xPZE7TCELcfYi4yXEHZZI3Pbh
oQIhAP/UsgJbsfH1GFv8Y8qGl5l/kmwwkwHhuKvEC87Yur9FAiEA/GlQv3ZfaXnT
lcCFT0aL02O0RDiRYyMUG/JAZQJs6CUCIQCHO5SZYIUwxIGK5mCNxxXOAzyQSiD7
hqkKywf+4FvfDQIhALa0TLyqJFom0t7c4iIGAIRc8UlIYQSPiajI64+x9775AiEA
0v4fgSK/Rq059zW1753JjuB6aR0Uh+3RqJII4dUR1Wg=
-----END RSA PRIVATE KEY-----
ubuntu-push-0.2+14.04.20140411/server/acceptance/ssl/testing.cert 0000644 0000153 0177776 00000001036 12322032412 024764 0 ustar pbuser nogroup 0000000 0000000 -----BEGIN CERTIFICATE-----
MIIBYzCCAQ+gAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD
bzAeFw0xMzEyMTkyMDU1NDNaFw0yMzEyMTcyMDU1NDNaMBIxEDAOBgNVBAoTB0Fj
bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAPw+niki17X2qALE2A2AzE1q5dvK
9CI4OduRtT9IgbFLC6psqAT21NA+QbY17nWSSpyP65zkMkwKXrbDzstwLPkCAwEA
AaNUMFIwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud
EwEB/wQFMAMBAf8wGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAsGCSqGSIb3
DQEBBQNBAFqiVI+Km2XPSO+pxITaPvhmuzg+XG3l1+2di3gL+HlDobocjBqRctRU
YySO32W07acjGJmCHUKpCJuq9X8hpmk=
-----END CERTIFICATE-----
ubuntu-push-0.2+14.04.20140411/server/acceptance/ssl/README 0000644 0000153 0177776 00000000303 12322032412 023304 0 ustar pbuser nogroup 0000000 0000000 testing.key/testing.cert
------------------------
Generated with:
go run /usr/lib/go/src/pkg/crypto/tls/generate_cert.go -ca -host localhost -rsa-bits 512 -duration 87600h
and then renamed.
ubuntu-push-0.2+14.04.20140411/server/session/ 0000755 0000153 0177776 00000000000 12322032700 021224 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/session/session.go 0000644 0000153 0177776 00000010346 12322032421 023242 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package session has code handling long-lived connections from devices.
package session
import (
"net"
"time"
"launchpad.net/ubuntu-push/protocol"
"launchpad.net/ubuntu-push/server/broker"
)
// SessionConfig is for carrying the session configuration.
type SessionConfig interface {
// pings are emitted each ping interval
PingInterval() time.Duration
// send and waiting for response shouldn't take more than exchange
// timeout
ExchangeTimeout() time.Duration
}
// sessionStart manages the start of the protocol session.
func sessionStart(proto protocol.Protocol, brkr broker.Broker, cfg SessionConfig) (broker.BrokerSession, error) {
var connMsg protocol.ConnectMsg
proto.SetDeadline(time.Now().Add(cfg.ExchangeTimeout()))
err := proto.ReadMessage(&connMsg)
if err != nil {
return nil, err
}
if connMsg.Type != "connect" {
return nil, &broker.ErrAbort{"expected CONNECT message"}
}
err = proto.WriteMessage(&protocol.ConnAckMsg{
Type: "connack",
Params: protocol.ConnAckParams{PingInterval: cfg.PingInterval().String()},
})
if err != nil {
return nil, err
}
return brkr.Register(&connMsg)
}
// exchange writes outMsg message, reads answer in inMsg
func exchange(proto protocol.Protocol, outMsg, inMsg interface{}, exchangeTimeout time.Duration) error {
proto.SetDeadline(time.Now().Add(exchangeTimeout))
err := proto.WriteMessage(outMsg)
if err != nil {
return err
}
if inMsg == nil { // no answer expected, breaking connection
return &broker.ErrAbort{"session broken for reason"}
}
err = proto.ReadMessage(inMsg)
if err != nil {
return err
}
return nil
}
// sessionLoop manages the exchanges of the protocol session.
func sessionLoop(proto protocol.Protocol, sess broker.BrokerSession, cfg SessionConfig, track SessionTracker) error {
pingInterval := cfg.PingInterval()
exchangeTimeout := cfg.ExchangeTimeout()
pingTimer := time.NewTimer(pingInterval)
intervalStart := time.Now()
ch := sess.SessionChannel()
Loop:
for {
select {
case <-pingTimer.C:
track.EffectivePingInterval(time.Since(intervalStart))
pingMsg := &protocol.PingPongMsg{"ping"}
var pongMsg protocol.PingPongMsg
err := exchange(proto, pingMsg, &pongMsg, exchangeTimeout)
if err != nil {
return err
}
if pongMsg.Type != "pong" {
return &broker.ErrAbort{"expected PONG message"}
}
pingTimer.Reset(pingInterval)
case exchg, ok := <-ch:
pingTimer.Stop()
if !ok {
return &broker.ErrAbort{"terminated"}
}
outMsg, inMsg, err := exchg.Prepare(sess)
if err == broker.ErrNop { // nothing to do
pingTimer.Reset(pingInterval)
intervalStart = time.Now()
continue Loop
}
if err != nil {
return err
}
for {
done := outMsg.Split()
err = exchange(proto, outMsg, inMsg, exchangeTimeout)
if err != nil {
return err
}
if done {
pingTimer.Reset(pingInterval)
intervalStart = time.Now()
}
err = exchg.Acked(sess, done)
if err != nil {
return err
}
if done {
break
}
}
}
}
}
// Session manages the session with a client.
func Session(conn net.Conn, brkr broker.Broker, cfg SessionConfig, track SessionTracker) error {
defer conn.Close()
track.Start(conn)
v, err := protocol.ReadWireFormatVersion(conn, cfg.ExchangeTimeout())
if err != nil {
return track.End(err)
}
if v != protocol.ProtocolWireVersion {
return track.End(&broker.ErrAbort{"unexpected wire format version"})
}
proto := protocol.NewProtocol0(conn)
sess, err := sessionStart(proto, brkr, cfg)
if err != nil {
return track.End(err)
}
track.Registered(sess)
defer brkr.Unregister(sess)
return track.End(sessionLoop(proto, sess, cfg, track))
}
ubuntu-push-0.2+14.04.20140411/server/session/tracker_test.go 0000644 0000153 0177776 00000004233 12322032412 024247 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package session
import (
"fmt"
"net"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/server/broker"
"launchpad.net/ubuntu-push/server/broker/testing"
helpers "launchpad.net/ubuntu-push/testing"
)
type trackerSuite struct {
testlog *helpers.TestLogger
}
func (s *trackerSuite) SetUpTest(c *C) {
s.testlog = helpers.NewTestLogger(c, "debug")
}
var _ = Suite(&trackerSuite{})
type testRemoteAddrable struct{}
func (tra *testRemoteAddrable) RemoteAddr() net.Addr {
return &net.TCPAddr{net.IPv4(127, 0, 0, 1), 9999, ""}
}
func (s *trackerSuite) TestSessionTrackStart(c *C) {
track := NewTracker(s.testlog)
track.Start(&testRemoteAddrable{})
c.Check(track.(*tracker).sessionId, Not(Equals), 0)
regExpected := fmt.Sprintf(`DEBUG session\(%x\) connected 127\.0\.0\.1:9999\n`, track.(*tracker).sessionId)
c.Check(s.testlog.Captured(), Matches, regExpected)
}
func (s *trackerSuite) TestSessionTrackRegistered(c *C) {
track := NewTracker(s.testlog)
track.Start(&testRemoteAddrable{})
track.Registered(&testing.TestBrokerSession{DeviceId: "DEV-ID"})
regExpected := fmt.Sprintf(`.*connected.*\nINFO session\(%x\) registered DEV-ID\n`, track.(*tracker).sessionId)
c.Check(s.testlog.Captured(), Matches, regExpected)
}
func (s *trackerSuite) TestSessionTrackEnd(c *C) {
track := NewTracker(s.testlog)
track.Start(&testRemoteAddrable{})
track.End(&broker.ErrAbort{})
regExpected := fmt.Sprintf(`.*connected.*\nDEBUG session\(%x\) ended with: session aborted \(\)\n`, track.(*tracker).sessionId)
c.Check(s.testlog.Captured(), Matches, regExpected)
}
ubuntu-push-0.2+14.04.20140411/server/session/tracker.go 0000644 0000153 0177776 00000003777 12322032412 023224 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package session
import (
"net"
"time"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/server/broker"
)
// SessionTracker logs session events.
type SessionTracker interface {
logger.Logger
// Session got started.
Start(WithRemoteAddr)
// Session got registered with broker as sess BrokerSession.
Registered(sess broker.BrokerSession)
// Report effective elapsed ping interval.
EffectivePingInterval(time.Duration)
// Session got ended with error err (can be nil).
End(error) error
}
// WithRemoteAddr can report a remote address.
type WithRemoteAddr interface {
RemoteAddr() net.Addr
}
var sessionsEpoch = time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC).UnixNano()
// Tracker implements SessionTracker simply.
type tracker struct {
logger.Logger
sessionId int64 // xxx use timeuuid later
}
func NewTracker(logger logger.Logger) SessionTracker {
return &tracker{Logger: logger}
}
func (trk *tracker) Start(conn WithRemoteAddr) {
trk.sessionId = time.Now().UnixNano() - sessionsEpoch
trk.Debugf("session(%x) connected %v", trk.sessionId, conn.RemoteAddr())
}
func (trk *tracker) Registered(sess broker.BrokerSession) {
trk.Infof("session(%x) registered %v", trk.sessionId, sess.DeviceIdentifier())
}
func (trk *tracker) EffectivePingInterval(time.Duration) {
}
func (trk *tracker) End(err error) error {
trk.Debugf("session(%x) ended with: %v", trk.sessionId, err)
return err
}
ubuntu-push-0.2+14.04.20140411/server/session/session_test.go 0000644 0000153 0177776 00000051071 12322032421 024301 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package session
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"reflect"
stdtesting "testing"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/protocol"
"launchpad.net/ubuntu-push/server/broker"
"launchpad.net/ubuntu-push/server/broker/testing"
helpers "launchpad.net/ubuntu-push/testing"
)
func TestSession(t *stdtesting.T) { TestingT(t) }
type sessionSuite struct {
testlog *helpers.TestLogger
}
func (s *sessionSuite) SetUpTest(c *C) {
s.testlog = helpers.NewTestLogger(c, "debug")
}
var _ = Suite(&sessionSuite{})
type testProtocol struct {
up chan interface{}
down chan interface{}
}
// takeNext takes a value from given channel with a 5s timeout
func takeNext(ch <-chan interface{}) interface{} {
select {
case <-time.After(5 * time.Second):
panic("test protocol exchange stuck: too long waiting")
case v := <-ch:
return v
}
return nil
}
func (c *testProtocol) SetDeadline(t time.Time) {
deadAfter := t.Sub(time.Now())
deadAfter = (deadAfter + time.Millisecond/2) / time.Millisecond * time.Millisecond
c.down <- fmt.Sprintf("deadline %v", deadAfter)
}
func (c *testProtocol) ReadMessage(dest interface{}) error {
switch v := takeNext(c.up).(type) {
case error:
return v
default:
// make sure JSON.Unmarshal works with dest
var marshalledMsg []byte
marshalledMsg, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("can't jsonify test value: %v", v)
}
return json.Unmarshal(marshalledMsg, dest)
}
return nil
}
func (c *testProtocol) WriteMessage(src interface{}) error {
// make sure JSON.Marshal works with src
_, err := json.Marshal(src)
if err != nil {
return err
}
val := reflect.ValueOf(src)
if val.Kind() == reflect.Ptr {
src = val.Elem().Interface()
}
c.down <- src
switch v := takeNext(c.up).(type) {
case error:
return v
}
return nil
}
type testSessionConfig struct {
exchangeTimeout time.Duration
pingInterval time.Duration
}
func (tsc *testSessionConfig) PingInterval() time.Duration {
return tsc.pingInterval
}
func (tsc *testSessionConfig) ExchangeTimeout() time.Duration {
return tsc.exchangeTimeout
}
var cfg10msPingInterval5msExchangeTout = &testSessionConfig{
pingInterval: 10 * time.Millisecond,
exchangeTimeout: 5 * time.Millisecond,
}
type testBroker struct {
registration chan interface{}
err error
}
func newTestBroker() *testBroker {
return &testBroker{registration: make(chan interface{}, 2)}
}
func (tb *testBroker) Register(connect *protocol.ConnectMsg) (broker.BrokerSession, error) {
tb.registration <- "register " + connect.DeviceId
return &testing.TestBrokerSession{DeviceId: connect.DeviceId}, tb.err
}
func (tb *testBroker) Unregister(sess broker.BrokerSession) {
tb.registration <- "unregister " + sess.DeviceIdentifier()
}
func (s *sessionSuite) TestSessionStart(c *C) {
var sess broker.BrokerSession
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
brkr := newTestBroker()
go func() {
var err error
sess, err = sessionStart(tp, brkr, cfg10msPingInterval5msExchangeTout)
errCh <- err
}()
c.Check(takeNext(down), Equals, "deadline 5ms")
up <- protocol.ConnectMsg{Type: "connect", ClientVer: "1", DeviceId: "dev-1"}
c.Check(takeNext(down), Equals, protocol.ConnAckMsg{
Type: "connack",
Params: protocol.ConnAckParams{(10 * time.Millisecond).String()},
})
up <- nil // no write error
err := <-errCh
c.Check(err, IsNil)
c.Check(takeNext(brkr.registration), Equals, "register dev-1")
c.Check(sess.DeviceIdentifier(), Equals, "dev-1")
}
func (s *sessionSuite) TestSessionRegisterError(c *C) {
var sess broker.BrokerSession
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
brkr := newTestBroker()
errRegister := errors.New("register failure")
brkr.err = errRegister
go func() {
var err error
sess, err = sessionStart(tp, brkr, cfg10msPingInterval5msExchangeTout)
errCh <- err
}()
up <- protocol.ConnectMsg{Type: "connect", ClientVer: "1", DeviceId: "dev-1"}
takeNext(down) // CONNACK
up <- nil // no write error
err := <-errCh
c.Check(err, Equals, errRegister)
}
func (s *sessionSuite) TestSessionStartReadError(c *C) {
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
up <- io.ErrUnexpectedEOF
_, err := sessionStart(tp, nil, cfg10msPingInterval5msExchangeTout)
c.Check(err, Equals, io.ErrUnexpectedEOF)
}
func (s *sessionSuite) TestSessionStartWriteError(c *C) {
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
up <- protocol.ConnectMsg{Type: "connect"}
up <- io.ErrUnexpectedEOF
_, err := sessionStart(tp, nil, cfg10msPingInterval5msExchangeTout)
c.Check(err, Equals, io.ErrUnexpectedEOF)
// sanity
c.Check(takeNext(down), Matches, "deadline.*")
c.Check(takeNext(down), FitsTypeOf, protocol.ConnAckMsg{})
}
func (s *sessionSuite) TestSessionStartMismatch(c *C) {
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
up <- protocol.ConnectMsg{Type: "what"}
_, err := sessionStart(tp, nil, cfg10msPingInterval5msExchangeTout)
c.Check(err, DeepEquals, &broker.ErrAbort{"expected CONNECT message"})
}
var cfg5msPingInterval2msExchangeTout = &testSessionConfig{
pingInterval: 5 * time.Millisecond,
exchangeTimeout: 2 * time.Millisecond,
}
func (s *sessionSuite) TestSessionLoop(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
sess := &testing.TestBrokerSession{}
go func() {
errCh <- sessionLoop(tp, sess, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), DeepEquals, protocol.PingPongMsg{Type: "ping"})
up <- nil // no write error
up <- protocol.PingPongMsg{Type: "pong"}
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), DeepEquals, protocol.PingPongMsg{Type: "ping"})
up <- nil // no write error
up <- io.ErrUnexpectedEOF
err := <-errCh
c.Check(err, Equals, io.ErrUnexpectedEOF)
}
func (s *sessionSuite) TestSessionLoopWriteError(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
sess := &testing.TestBrokerSession{}
go func() {
errCh <- sessionLoop(tp, sess, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), FitsTypeOf, protocol.PingPongMsg{})
up <- io.ErrUnexpectedEOF // write error
err := <-errCh
c.Check(err, Equals, io.ErrUnexpectedEOF)
}
func (s *sessionSuite) TestSessionLoopMismatch(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
sess := &testing.TestBrokerSession{}
go func() {
errCh <- sessionLoop(tp, sess, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), DeepEquals, protocol.PingPongMsg{Type: "ping"})
up <- nil // no write error
up <- protocol.PingPongMsg{Type: "what"}
err := <-errCh
c.Check(err, DeepEquals, &broker.ErrAbort{"expected PONG message"})
}
type testMsg struct {
Type string `json:"T"`
Part int `json:"P"`
nParts int
}
func (m *testMsg) Split() bool {
if m.nParts == 0 {
return true
}
m.Part++
if m.Part == m.nParts {
return true
}
return false
}
type testExchange struct {
inMsg testMsg
prepErr error
finErr error
finSleep time.Duration
nParts int
done chan interface{}
}
func (exchg *testExchange) Prepare(sess broker.BrokerSession) (outMsg protocol.SplittableMsg, inMsg interface{}, err error) {
return &testMsg{Type: "msg", nParts: exchg.nParts}, &exchg.inMsg, exchg.prepErr
}
func (exchg *testExchange) Acked(sess broker.BrokerSession, done bool) error {
time.Sleep(exchg.finSleep)
if exchg.done != nil {
var doneStr string
if done {
doneStr = "y"
} else {
doneStr = "n"
}
exchg.done <- doneStr
}
return exchg.finErr
}
func (s *sessionSuite) TestSessionLoopExchange(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
exchanges := make(chan broker.Exchange, 1)
exchanges <- &testExchange{}
sess := &testing.TestBrokerSession{Exchanges: exchanges}
go func() {
errCh <- sessionLoop(tp, sess, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), DeepEquals, testMsg{Type: "msg"})
up <- nil // no write error
up <- testMsg{Type: "ack"}
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), DeepEquals, protocol.PingPongMsg{Type: "ping"})
up <- nil // no write error
up <- io.EOF
err := <-errCh
c.Check(err, Equals, io.EOF)
}
func (s *sessionSuite) TestSessionLoopKick(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
exchanges := make(chan broker.Exchange, 1)
sess := &testing.TestBrokerSession{Exchanges: exchanges}
go func() {
errCh <- sessionLoop(tp, sess, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
close(exchanges)
err := <-errCh
c.Check(err, DeepEquals, &broker.ErrAbort{"terminated"})
}
func (s *sessionSuite) TestSessionLoopExchangeErrNop(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
exchanges := make(chan broker.Exchange, 1)
exchanges <- &testExchange{prepErr: broker.ErrNop}
sess := &testing.TestBrokerSession{Exchanges: exchanges}
go func() {
errCh <- sessionLoop(tp, sess, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), DeepEquals, protocol.PingPongMsg{Type: "ping"})
up <- nil // no write error
up <- io.EOF
err := <-errCh
c.Check(err, Equals, io.EOF)
}
func (s *sessionSuite) TestSessionLoopExchangeSplit(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
exchanges := make(chan broker.Exchange, 1)
exchange := &testExchange{nParts: 2, done: make(chan interface{}, 2)}
exchanges <- exchange
sess := &testing.TestBrokerSession{Exchanges: exchanges}
go func() {
errCh <- sessionLoop(tp, sess, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), DeepEquals, testMsg{Type: "msg", Part: 1, nParts: 2})
up <- nil // no write error
up <- testMsg{Type: "ack"}
c.Check(takeNext(exchange.done), Equals, "n")
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), DeepEquals, testMsg{Type: "msg", Part: 2, nParts: 2})
up <- nil // no write error
up <- testMsg{Type: "ack"}
c.Check(takeNext(exchange.done), Equals, "y")
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), DeepEquals, protocol.PingPongMsg{Type: "ping"})
up <- nil // no write error
up <- io.EOF
err := <-errCh
c.Check(err, Equals, io.EOF)
}
func (s *sessionSuite) TestSessionLoopExchangePrepareError(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
exchanges := make(chan broker.Exchange, 1)
prepErr := errors.New("prepare failure")
exchanges <- &testExchange{prepErr: prepErr}
sess := &testing.TestBrokerSession{Exchanges: exchanges}
go func() {
errCh <- sessionLoop(tp, sess, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
err := <-errCh
c.Check(err, Equals, prepErr)
}
func (s *sessionSuite) TestSessionLoopExchangeAckedError(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
exchanges := make(chan broker.Exchange, 1)
finErr := errors.New("finish error")
exchanges <- &testExchange{finErr: finErr}
sess := &testing.TestBrokerSession{Exchanges: exchanges}
go func() {
errCh <- sessionLoop(tp, sess, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), DeepEquals, testMsg{Type: "msg"})
up <- nil // no write error
up <- testMsg{Type: "ack"}
err := <-errCh
c.Check(err, Equals, finErr)
}
func (s *sessionSuite) TestSessionLoopExchangeWriteError(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
exchanges := make(chan broker.Exchange, 1)
exchanges <- &testExchange{}
sess := &testing.TestBrokerSession{Exchanges: exchanges}
go func() {
errCh <- sessionLoop(tp, sess, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), FitsTypeOf, testMsg{})
up <- io.ErrUnexpectedEOF
err := <-errCh
c.Check(err, Equals, io.ErrUnexpectedEOF)
}
func (s *sessionSuite) TestSessionLoopConnBrokenExchange(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
exchanges := make(chan broker.Exchange, 1)
exchanges <- &broker.ConnBrokenExchange{"REASON"}
sess := &testing.TestBrokerSession{Exchanges: exchanges}
go func() {
errCh <- sessionLoop(tp, sess, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
c.Check(takeNext(down), Equals, "deadline 2ms")
c.Check(takeNext(down), DeepEquals, protocol.ConnBrokenMsg{"connbroken", "REASON"})
up <- nil // no write error
err := <-errCh
c.Check(err, DeepEquals, &broker.ErrAbort{"session broken for reason"})
}
type testTracker struct {
SessionTracker
interval chan interface{}
}
func (trk *testTracker) EffectivePingInterval(interval time.Duration) {
trk.interval <- interval
}
var cfg50msPingInterval = &testSessionConfig{
pingInterval: 50 * time.Millisecond,
exchangeTimeout: 10 * time.Millisecond,
}
func (s *sessionSuite) TestSessionLoopExchangeNextPing(c *C) {
track := &testTracker{NewTracker(s.testlog), make(chan interface{}, 1)}
errCh := make(chan error, 1)
up := make(chan interface{}, 5)
down := make(chan interface{}, 5)
tp := &testProtocol{up, down}
exchanges := make(chan broker.Exchange, 1)
exchanges <- &testExchange{finSleep: 15 * time.Millisecond}
sess := &testing.TestBrokerSession{Exchanges: exchanges}
go func() {
errCh <- sessionLoop(tp, sess, cfg50msPingInterval, track)
}()
c.Check(takeNext(down), Equals, "deadline 10ms")
c.Check(takeNext(down), DeepEquals, testMsg{Type: "msg"})
up <- nil // no write error
up <- testMsg{Type: "ack"}
// next ping interval starts around here
interval := takeNext(track.interval).(time.Duration)
c.Check(takeNext(down), Equals, "deadline 10ms")
c.Check(takeNext(down), DeepEquals, protocol.PingPongMsg{Type: "ping"})
effectiveOfPing := float64(interval) / float64(50*time.Millisecond)
comment := Commentf("effectiveOfPing=%f", effectiveOfPing)
c.Check(effectiveOfPing > 0.95, Equals, true, comment)
c.Check(effectiveOfPing < 1.15, Equals, true, comment)
up <- nil // no write error
up <- io.EOF
err := <-errCh
c.Check(err, Equals, io.EOF)
}
func serverClientWire() (srv net.Conn, cli net.Conn, lst net.Listener) {
lst, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
cli, err = net.DialTCP("tcp", nil, lst.Addr().(*net.TCPAddr))
if err != nil {
panic(err)
}
srv, err = lst.Accept()
if err != nil {
panic(err)
}
return
}
type rememberDeadlineConn struct {
net.Conn
deadlineKind []string
}
func (c *rememberDeadlineConn) SetDeadline(t time.Time) error {
c.deadlineKind = append(c.deadlineKind, "both")
return c.Conn.SetDeadline(t)
}
func (c *rememberDeadlineConn) SetReadDeadline(t time.Time) error {
c.deadlineKind = append(c.deadlineKind, "read")
return c.Conn.SetDeadline(t)
}
func (c *rememberDeadlineConn) SetWriteDeadline(t time.Time) error {
c.deadlineKind = append(c.deadlineKind, "write")
return c.Conn.SetDeadline(t)
}
func (s *sessionSuite) TestSessionWire(c *C) {
track := NewTracker(s.testlog)
errCh := make(chan error, 1)
srv, cli, lst := serverClientWire()
defer lst.Close()
remSrv := &rememberDeadlineConn{srv, make([]string, 0, 2)}
brkr := newTestBroker()
go func() {
errCh <- Session(remSrv, brkr, cfg50msPingInterval, track)
}()
io.WriteString(cli, "\x00")
io.WriteString(cli, "\x00\x20{\"T\":\"connect\",\"DeviceId\":\"DEV\"}")
// connack
downStream := bufio.NewReader(cli)
msg, err := downStream.ReadBytes(byte('}'))
c.Check(err, IsNil)
c.Check(msg, DeepEquals, []byte("\x00\x30{\"T\":\"connack\",\"Params\":{\"PingInterval\":\"50ms\"}"))
// eat the last }
rbr, err := downStream.ReadByte()
c.Check(err, IsNil)
c.Check(rbr, Equals, byte('}'))
// first ping
msg, err = downStream.ReadBytes(byte('}'))
c.Check(err, IsNil)
c.Check(msg, DeepEquals, []byte("\x00\x0c{\"T\":\"ping\"}"))
c.Check(takeNext(brkr.registration), Equals, "register DEV")
c.Check(len(brkr.registration), Equals, 0) // not yet unregistered
cli.Close()
err = <-errCh
c.Check(remSrv.deadlineKind, DeepEquals, []string{"read", "both", "both"})
c.Check(err, Equals, io.EOF)
c.Check(takeNext(brkr.registration), Equals, "unregister DEV")
// tracking
c.Check(s.testlog.Captured(), Matches, `.*connected.*\n.*registered DEV.*\n.*ended with: EOF\n`)
}
func (s *sessionSuite) TestSessionWireTimeout(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
srv, cli, lst := serverClientWire()
defer lst.Close()
remSrv := &rememberDeadlineConn{srv, make([]string, 0, 2)}
brkr := newTestBroker()
go func() {
errCh <- Session(remSrv, brkr, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
err := <-errCh
c.Check(err, FitsTypeOf, &net.OpError{})
c.Check(remSrv.deadlineKind, DeepEquals, []string{"read"})
cli.Close()
}
func (s *sessionSuite) TestSessionWireWrongVersion(c *C) {
track := NewTracker(s.testlog)
errCh := make(chan error, 1)
srv, cli, lst := serverClientWire()
defer lst.Close()
brkr := newTestBroker()
go func() {
errCh <- Session(srv, brkr, cfg50msPingInterval, track)
}()
io.WriteString(cli, "\x10")
err := <-errCh
c.Check(err, DeepEquals, &broker.ErrAbort{"unexpected wire format version"})
cli.Close()
// tracking
c.Check(s.testlog.Captured(), Matches, `.*connected.*\n.*ended with: session aborted \(unexpected.*version\)\n`)
}
func (s *sessionSuite) TestSessionWireEarlyClose(c *C) {
track := NewTracker(s.testlog)
errCh := make(chan error, 1)
srv, cli, lst := serverClientWire()
defer lst.Close()
brkr := newTestBroker()
go func() {
errCh <- Session(srv, brkr, cfg50msPingInterval, track)
}()
cli.Close()
err := <-errCh
c.Check(err, Equals, io.EOF)
// tracking
c.Check(s.testlog.Captured(), Matches, `.*connected.*\n.*ended with: EOF\n`)
}
func (s *sessionSuite) TestSessionWireEarlyClose2(c *C) {
track := NewTracker(s.testlog)
errCh := make(chan error, 1)
srv, cli, lst := serverClientWire()
defer lst.Close()
brkr := newTestBroker()
go func() {
errCh <- Session(srv, brkr, cfg50msPingInterval, track)
}()
io.WriteString(cli, "\x00")
io.WriteString(cli, "\x00")
cli.Close()
err := <-errCh
c.Check(err, Equals, io.EOF)
// tracking
c.Check(s.testlog.Captured(), Matches, `.*connected.*\n.*ended with: EOF\n`)
}
func (s *sessionSuite) TestSessionWireTimeout2(c *C) {
nopTrack := NewTracker(s.testlog)
errCh := make(chan error, 1)
srv, cli, lst := serverClientWire()
defer lst.Close()
remSrv := &rememberDeadlineConn{srv, make([]string, 0, 2)}
brkr := newTestBroker()
go func() {
errCh <- Session(remSrv, brkr, cfg5msPingInterval2msExchangeTout, nopTrack)
}()
io.WriteString(cli, "\x00")
io.WriteString(cli, "\x00")
err := <-errCh
c.Check(err, FitsTypeOf, &net.OpError{})
c.Check(remSrv.deadlineKind, DeepEquals, []string{"read", "both"})
cli.Close()
}
ubuntu-push-0.2+14.04.20140411/server/runner_test.go 0000644 0000153 0177776 00000010062 12322032412 022437 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package server
import (
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/config"
helpers "launchpad.net/ubuntu-push/testing"
)
type runnerSuite struct {
prevBootLogListener func(string, net.Listener)
prevBootLogFatalf func(string, ...interface{})
lst net.Listener
kind string
}
var _ = Suite(&runnerSuite{})
func (s *runnerSuite) SetUpSuite(c *C) {
s.prevBootLogFatalf = BootLogFatalf
s.prevBootLogListener = BootLogListener
BootLogFatalf = func(format string, v ...interface{}) {
panic(fmt.Sprintf(format, v...))
}
BootLogListener = func(kind string, lst net.Listener) {
s.kind = kind
s.lst = lst
}
}
func (s *runnerSuite) TearDownSuite(c *C) {
BootLogListener = s.prevBootLogListener
BootLogFatalf = s.prevBootLogFatalf
}
var testHTTPServeParsedConfig = HTTPServeParsedConfig{
"127.0.0.1:0",
config.ConfigTimeDuration{5 * time.Second},
config.ConfigTimeDuration{5 * time.Second},
}
func testHandle(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "yay!\n")
}
func (s *runnerSuite) TestHTTPServeRunner(c *C) {
errCh := make(chan interface{}, 1)
h := http.HandlerFunc(testHandle)
runner := HTTPServeRunner(nil, h, &testHTTPServeParsedConfig)
c.Assert(s.lst, Not(IsNil))
defer s.lst.Close()
c.Check(s.kind, Equals, "http")
go func() {
defer func() {
errCh <- recover()
}()
runner()
}()
resp, err := http.Get(fmt.Sprintf("http://%s/", s.lst.Addr()))
c.Assert(err, IsNil)
defer resp.Body.Close()
c.Assert(resp.StatusCode, Equals, 200)
body, err := ioutil.ReadAll(resp.Body)
c.Assert(err, IsNil)
c.Check(string(body), Equals, "yay!\n")
s.lst.Close()
c.Check(<-errCh, Matches, "accepting http connections:.*closed.*")
}
var testDevicesParsedConfig = DevicesParsedConfig{
ParsedPingInterval: config.ConfigTimeDuration{60 * time.Second},
ParsedExchangeTimeout: config.ConfigTimeDuration{10 * time.Second},
ParsedBrokerQueueSize: config.ConfigQueueSize(1000),
ParsedSessionQueueSize: config.ConfigQueueSize(10),
ParsedAddr: "127.0.0.1:0",
ParsedKeyPEMFile: "",
ParsedCertPEMFile: "",
keyPEMBlock: helpers.TestKeyPEMBlock,
certPEMBlock: helpers.TestCertPEMBlock,
}
func (s *runnerSuite) TestDevicesRunner(c *C) {
prevBootLogger := BootLogger
testlog := helpers.NewTestLogger(c, "debug")
BootLogger = testlog
defer func() {
BootLogger = prevBootLogger
}()
runner := DevicesRunner(nil, func(conn net.Conn) error { return nil }, BootLogger, &testDevicesParsedConfig)
c.Assert(s.lst, Not(IsNil))
s.lst.Close()
c.Check(s.kind, Equals, "devices")
c.Check(runner, PanicMatches, "accepting device connections:.*closed.*")
}
func (s *runnerSuite) TestDevicesRunnerAdoptListener(c *C) {
prevBootLogger := BootLogger
testlog := helpers.NewTestLogger(c, "debug")
BootLogger = testlog
defer func() {
BootLogger = prevBootLogger
}()
lst0, err := net.Listen("tcp", "127.0.0.1:0")
c.Assert(err, IsNil)
defer lst0.Close()
DevicesRunner(lst0, func(conn net.Conn) error { return nil }, BootLogger, &testDevicesParsedConfig)
c.Assert(s.lst, Not(IsNil))
c.Check(s.lst.Addr().String(), Equals, lst0.Addr().String())
s.lst.Close()
}
func (s *runnerSuite) TestHTTPServeRunnerAdoptListener(c *C) {
lst0, err := net.Listen("tcp", "127.0.0.1:0")
c.Assert(err, IsNil)
defer lst0.Close()
HTTPServeRunner(lst0, nil, &testHTTPServeParsedConfig)
c.Assert(s.lst, Equals, lst0)
c.Check(s.kind, Equals, "http")
}
ubuntu-push-0.2+14.04.20140411/server/store/ 0000755 0000153 0177776 00000000000 12322032700 020675 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/server/store/inmemory_test.go 0000644 0000153 0177776 00000005131 12322032412 024122 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package store
import (
"encoding/json"
"time"
. "launchpad.net/gocheck"
)
type inMemorySuite struct{}
var _ = Suite(&inMemorySuite{})
func (s *inMemorySuite) TestGetInternalChannelId(c *C) {
sto := NewInMemoryPendingStore()
chanId, err := sto.GetInternalChannelId("system")
c.Check(err, IsNil)
c.Check(chanId, Equals, SystemInternalChannelId)
chanId, err = sto.GetInternalChannelId("blah")
c.Check(err, Equals, ErrUnknownChannel)
c.Check(chanId, Equals, InternalChannelId(""))
}
func (s *inMemorySuite) TestGetChannelSnapshotEmpty(c *C) {
sto := NewInMemoryPendingStore()
top, res, err := sto.GetChannelSnapshot(SystemInternalChannelId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(0))
c.Check(res, DeepEquals, []json.RawMessage(nil))
}
func (s *inMemorySuite) TestAppendToChannelAndGetChannelSnapshot(c *C) {
sto := NewInMemoryPendingStore()
notification1 := json.RawMessage(`{"a":1}`)
notification2 := json.RawMessage(`{"a":2}`)
muchLater := time.Now().Add(time.Minute)
sto.AppendToChannel(SystemInternalChannelId, notification1, muchLater)
sto.AppendToChannel(SystemInternalChannelId, notification2, muchLater)
top, res, err := sto.GetChannelSnapshot(SystemInternalChannelId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(2))
c.Check(res, DeepEquals, []json.RawMessage{notification1, notification2})
}
func (s *inMemorySuite) TestAppendToChannelAndGetChannelSnapshotWithExpiration(c *C) {
sto := NewInMemoryPendingStore()
notification1 := json.RawMessage(`{"a":1}`)
notification2 := json.RawMessage(`{"a":2}`)
verySoon := time.Now().Add(100 * time.Millisecond)
muchLater := time.Now().Add(time.Minute)
sto.AppendToChannel(SystemInternalChannelId, notification1, muchLater)
sto.AppendToChannel(SystemInternalChannelId, notification2, verySoon)
time.Sleep(200 * time.Millisecond)
top, res, err := sto.GetChannelSnapshot(SystemInternalChannelId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(2))
c.Check(res, DeepEquals, []json.RawMessage{notification1})
}
ubuntu-push-0.2+14.04.20140411/server/store/store.go 0000644 0000153 0177776 00000004520 12322032412 022361 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package store takes care of storing pending notifications.
package store
import (
"encoding/hex"
"encoding/json"
"errors"
"time"
)
type InternalChannelId string
var ErrUnknownChannel = errors.New("unknown channel name")
var ErrFull = errors.New("channel is full")
var ErrExpected128BitsHexRepr = errors.New("expected 128 bits hex repr")
const SystemInternalChannelId = InternalChannelId("0")
func InternalChannelIdToHex(chanId InternalChannelId) string {
if chanId == SystemInternalChannelId {
return "0"
}
panic("general InternalChannelIdToHex not implemeted yet")
}
var zero128 [16]byte
const noId = InternalChannelId("")
func HexToInternalChannelId(hexRepr string) (InternalChannelId, error) {
if hexRepr == "0" {
return SystemInternalChannelId, nil
}
if len(hexRepr) != 32 {
return noId, ErrExpected128BitsHexRepr
}
var idbytes [16]byte
_, err := hex.Decode(idbytes[:], []byte(hexRepr))
if err != nil {
return noId, ErrExpected128BitsHexRepr
}
if idbytes == zero128 {
return SystemInternalChannelId, nil
}
return InternalChannelId(idbytes[:]), nil
}
// PendingStore let store notifications into channels.
type PendingStore interface {
// GetInternalChannelId returns the internal store id for a channel
// given the name.
GetInternalChannelId(name string) (InternalChannelId, error)
// AppendToChannel appends a notification to the channel.
AppendToChannel(chanId InternalChannelId, notification json.RawMessage, expiration time.Time) error
// GetChannelSnapshot gets all the current notifications and
// current top level in the channel.
GetChannelSnapshot(chanId InternalChannelId) (topLevel int64, payloads []json.RawMessage, err error)
// Close is to be called when done with the store.
Close()
}
ubuntu-push-0.2+14.04.20140411/server/store/inmemory.go 0000644 0000153 0177776 00000005035 12322032412 023066 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package store
import (
"encoding/json"
"sync"
"time"
)
// one stored notification
type notification struct {
payload json.RawMessage
expiration time.Time
}
// one stored channel
type channel struct {
topLevel int64
notifications []notification
}
// InMemoryPendingStore is a basic in-memory pending notification store.
type InMemoryPendingStore struct {
lock sync.Mutex
store map[InternalChannelId]*channel
}
// NewInMemoryPendingStore returns a new InMemoryStore.
func NewInMemoryPendingStore() *InMemoryPendingStore {
return &InMemoryPendingStore{
store: make(map[InternalChannelId]*channel),
}
}
func (sto *InMemoryPendingStore) GetInternalChannelId(name string) (InternalChannelId, error) {
if name == "system" {
return SystemInternalChannelId, nil
}
return InternalChannelId(""), ErrUnknownChannel
}
func (sto *InMemoryPendingStore) AppendToChannel(chanId InternalChannelId, notificationPayload json.RawMessage, expiration time.Time) error {
sto.lock.Lock()
defer sto.lock.Unlock()
prev := sto.store[chanId]
if prev == nil {
prev = &channel{}
}
prev.topLevel++
prev.notifications = append(prev.notifications, notification{
payload: notificationPayload,
expiration: expiration,
})
sto.store[chanId] = prev
return nil
}
func (sto *InMemoryPendingStore) GetChannelSnapshot(chanId InternalChannelId) (int64, []json.RawMessage, error) {
sto.lock.Lock()
defer sto.lock.Unlock()
channel, ok := sto.store[chanId]
if !ok {
return 0, nil, nil
}
topLevel := channel.topLevel
n := len(channel.notifications)
res := make([]json.RawMessage, 0, n)
now := time.Now()
for _, notification := range channel.notifications {
if notification.expiration.Before(now) {
continue
}
res = append(res, notification.payload)
}
return topLevel, res, nil
}
func (sto *InMemoryPendingStore) Close() {
// ignored
}
// sanity check we implement the interface
var _ PendingStore = (*InMemoryPendingStore)(nil)
ubuntu-push-0.2+14.04.20140411/server/store/store_test.go 0000644 0000153 0177776 00000003442 12322032412 023422 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package store
import (
// "fmt"
"testing"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/protocol"
)
func TestStore(t *testing.T) { TestingT(t) }
type storeSuite struct{}
var _ = Suite(&storeSuite{})
func (s *storeSuite) TestInternalChannelIdToHex(c *C) {
c.Check(InternalChannelIdToHex(SystemInternalChannelId), Equals, protocol.SystemChannelId)
}
func (s *storeSuite) TestHexToInternalChannelId(c *C) {
i0, err := HexToInternalChannelId("0")
c.Check(err, IsNil)
c.Check(i0, Equals, SystemInternalChannelId)
i1, err := HexToInternalChannelId("00000000000000000000000000000000")
c.Check(err, IsNil)
c.Check(i1, Equals, SystemInternalChannelId)
i2, err := HexToInternalChannelId("f1c9bf7096084cb2a154979ce00c7f50")
c.Check(err, IsNil)
c.Check(i2, Equals, InternalChannelId("\xf1\xc9\xbf\x70\x96\x08\x4c\xb2\xa1\x54\x97\x9c\xe0\x0c\x7f\x50"))
_, err = HexToInternalChannelId("01")
c.Check(err, Equals, ErrExpected128BitsHexRepr)
_, err = HexToInternalChannelId("abceddddddddddddddddzeeeeeeeeeee")
c.Check(err, Equals, ErrExpected128BitsHexRepr)
_, err = HexToInternalChannelId("f1c9bf7096084cb2a154979ce00c7f50ff")
c.Check(err, Equals, ErrExpected128BitsHexRepr)
}
ubuntu-push-0.2+14.04.20140411/server/bootlog.go 0000644 0000153 0177776 00000002123 12322032412 021533 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package server
import (
"net"
"os"
"launchpad.net/ubuntu-push/logger"
)
// boot logging and hooks
func bootLogListener(kind string, lst net.Listener) {
BootLogger.Infof("listening for %s on %v", kind, lst.Addr())
}
var (
BootLogger = logger.NewSimpleLogger(os.Stderr, "debug")
// Boot logging helpers through BootLogger.
BootLogListener func(kind string, lst net.Listener) = bootLogListener
BootLogFatalf = BootLogger.Fatalf
)
ubuntu-push-0.2+14.04.20140411/logger/ 0000755 0000153 0177776 00000000000 12322032700 017512 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/logger/logger_test.go 0000644 0000153 0177776 00000007430 12322032412 022363 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package logger
import (
"bytes"
"fmt"
"log"
"os"
"runtime"
"testing"
. "launchpad.net/gocheck"
)
func TestLogger(t *testing.T) { TestingT(t) }
type loggerSuite struct{}
var _ = Suite(&loggerSuite{})
func (s *loggerSuite) TestErrorf(c *C) {
buf := &bytes.Buffer{}
logger := NewSimpleLogger(buf, "error")
logger.Errorf("%v %d", "error", 1)
c.Check(buf.String(), Matches, ".* ERROR error 1\n")
}
func (s *loggerSuite) TestFatalf(c *C) {
defer func() {
osExit = os.Exit
}()
var exitCode int
osExit = func(code int) {
exitCode = code
}
buf := &bytes.Buffer{}
logger := NewSimpleLogger(buf, "error")
logger.Fatalf("%v %v", "error", "fatal")
c.Check(buf.String(), Matches, ".* ERROR error fatal\n")
c.Check(exitCode, Equals, 1)
}
func (s *loggerSuite) TestInfof(c *C) {
buf := &bytes.Buffer{}
logger := NewSimpleLogger(buf, "info")
logger.Infof("%v %d", "info", 1)
c.Check(buf.String(), Matches, ".* INFO info 1\n")
}
func (s *loggerSuite) TestDebugf(c *C) {
buf := &bytes.Buffer{}
logger := NewSimpleLogger(buf, "debug")
logger.Debugf("%v %d", "debug", 1)
c.Check(buf.String(), Matches, `.* DEBUG debug 1\n`)
}
func (s *loggerSuite) TestFormat(c *C) {
buf := &bytes.Buffer{}
logger := NewSimpleLogger(buf, "error")
logger.Errorf("%v %d", "error", 2)
c.Check(buf.String(), Matches, `.* .*\.\d+ ERROR error 2\n`)
}
func (s *loggerSuite) TestLevel(c *C) {
buf := &bytes.Buffer{}
logger := NewSimpleLogger(buf, "error")
logger.Errorf("%s%d", "e", 3)
logger.Infof("%s%d", "i", 3)
logger.Debugf("%s%d", "d", 3)
c.Check(buf.String(), Matches, `.* ERROR e3\n`)
buf.Reset()
logger = NewSimpleLogger(buf, "info")
logger.Errorf("%s%d", "e", 4)
logger.Debugf("%s%d", "d", 4)
logger.Infof("%s%d", "i", 4)
c.Check(buf.String(), Matches, `.* ERROR e4\n.* INFO i4\n`)
buf.Reset()
logger = NewSimpleLogger(buf, "debug")
logger.Errorf("%s%d", "e", 5)
logger.Debugf("%s%d", "d", 5)
logger.Infof("%s%d", "i", 5)
c.Check(buf.String(), Matches, `.* ERROR e5\n.* DEBUG d5\n.* INFO i5\n`)
}
func panicAndRecover(logger Logger, n int, doPanic bool, line *int, ok *bool) {
defer func() {
if err := recover(); err != nil {
logger.PanicStackf("%v %d", err, n)
}
}()
_, _, *line, *ok = runtime.Caller(0)
if doPanic {
panic("Troubles") // @ line + 2
}
}
func (s *loggerSuite) TestPanicStackfPanicScenario(c *C) {
buf := &bytes.Buffer{}
logger := NewSimpleLogger(buf, "error")
var line int
var ok bool
panicAndRecover(logger, 6, true, &line, &ok)
c.Assert(ok, Equals, true)
c.Check(buf.String(), Matches, fmt.Sprintf("(?s).* ERROR\\(PANIC\\) Troubles 6:.*panicAndRecover.*logger_test.go:%d.*", line+2))
}
func (s *loggerSuite) TestPanicStackfNoPanicScenario(c *C) {
buf := &bytes.Buffer{}
logger := NewSimpleLogger(buf, "error")
var line int
var ok bool
panicAndRecover(logger, 6, false, &line, &ok)
c.Check(buf.String(), Equals, "")
}
func (s *loggerSuite) TestReexposeOutput(c *C) {
buf := &bytes.Buffer{}
baselog := log.New(buf, "", log.Lshortfile)
logger := NewSimpleLoggerFromMinimalLogger(baselog, "error")
baselog.Output(1, "foobar")
logger.Output(1, "foobaz")
c.Check(buf.String(), Matches, "logger_test.go:[0-9]+: foobar\nlogger_test.go:[0-9]+: foobaz\n")
}
ubuntu-push-0.2+14.04.20140411/logger/logger.go 0000644 0000153 0177776 00000006652 12322032412 021331 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package logger defines a simple logger API with level of logging control.
package logger
import (
"fmt"
"io"
"log"
"os"
"runtime"
)
// Logger is a simple logger interface with logging at levels.
type Logger interface {
// Re-expose base Output for logging events.
Output(calldept int, s string) error
// Errorf logs an error.
Errorf(format string, v ...interface{})
// Fatalf logs an error and exits the program with os.Exit(1).
Fatalf(format string, v ...interface{})
// PanicStackf logs an error message and a stacktrace, for use
// in panic recovery.
PanicStackf(format string, v ...interface{})
// Infof logs an info message.
Infof(format string, v ...interface{})
// Debugf logs a debug message.
Debugf(format string, v ...interface{})
}
type simpleLogger struct {
outputFunc func(calldepth int, s string) error
nlevel int
}
const (
lError = iota
lInfo
lDebug
)
var levelToNLevel = map[string]int{
"error": lError,
"info": lInfo,
"debug": lDebug,
}
// MinimalLogger is the minimal interface required to build a simple logger.
type MinimalLogger interface {
Output(calldepth int, s string) error
}
// NewSimpleLoggerFromMinimalLogger creates a logger logging only up
// to the given level. The level can be, in order: "error", "info",
// "debug". It takes a value just implementing stlib Logger.Output().
func NewSimpleLoggerFromMinimalLogger(minLog MinimalLogger, level string) Logger {
nlevel := levelToNLevel[level]
return &simpleLogger{
minLog.Output,
nlevel,
}
}
// NewSimpleLogger creates a logger logging only up to the given
// level. The level can be, in order: "error", "info", "debug". It takes an
// io.Writer.
func NewSimpleLogger(w io.Writer, level string) Logger {
return NewSimpleLoggerFromMinimalLogger(
log.New(w, "", log.Ldate|log.Ltime|log.Lmicroseconds),
level,
)
}
func (lg *simpleLogger) Output(calldepth int, s string) error {
return lg.outputFunc(calldepth+2, s)
}
func (lg *simpleLogger) Errorf(format string, v ...interface{}) {
lg.outputFunc(2, fmt.Sprintf("ERROR "+format, v...))
}
var osExit = os.Exit // for testing
func (lg *simpleLogger) Fatalf(format string, v ...interface{}) {
lg.outputFunc(2, fmt.Sprintf("ERROR "+format, v...))
osExit(1)
}
func (lg *simpleLogger) PanicStackf(format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
stack := make([]byte, 8*1024) // Stack writes less but doesn't fail
stackWritten := runtime.Stack(stack, false)
stack = stack[:stackWritten]
lg.outputFunc(2, fmt.Sprintf("ERROR(PANIC) %s:\n%s", msg, stack))
}
func (lg *simpleLogger) Infof(format string, v ...interface{}) {
if lg.nlevel >= lInfo {
lg.outputFunc(2, fmt.Sprintf("INFO "+format, v...))
}
}
func (lg *simpleLogger) Debugf(format string, v ...interface{}) {
if lg.nlevel >= lDebug {
lg.outputFunc(2, fmt.Sprintf("DEBUG "+format, v...))
}
}
ubuntu-push-0.2+14.04.20140411/protocol/ 0000755 0000153 0177776 00000000000 12322032700 020074 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/protocol/state-diag-session.gv 0000644 0000153 0177776 00000001727 12322032412 024144 0 ustar pbuser nogroup 0000000 0000000 digraph state_diagram_session {
label = "State diagram for session";
size="12,6";
rankdir=LR;
node [shape = circle];
start1 -> start2 [ label = "Read wire version" ];
start2 -> start3 [ label = "Read CONNECT" ];
start3 -> loop [ label = "Write CONNACK" ];
loop -> ping [ label = "Elapsed ping interval" ];
loop -> broadcast [label = "Receive broadcast request"];
ping -> pong_wait [label = "Write PING"];
broadcast -> ack_wait [label = "Write BROADCAST [fits one wire msg]"];
broadcast -> split_broadcast [label = "BROADCAST does not fit one wire msg"];
pong_wait -> loop [label = "Read PONG"];
ack_wait -> loop [label = "Read ACK"];
// split messages
split_broadcast -> split_ack_wait [label = "Write split BROADCAST"];
split_ack_wait -> split_broadcast [label = "Read ACK"];
split_broadcast -> loop [label = "All split msgs written"];
}
ubuntu-push-0.2+14.04.20140411/protocol/protocol.go 0000644 0000153 0177776 00000005722 12322032412 022272 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package protocol implements the client-daemon <-> push-server protocol.
package protocol
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net"
"time"
)
// Protocol is a connection capable of writing and reading the wire format
// of protocol messages.
type Protocol interface {
SetDeadline(t time.Time)
ReadMessage(msg interface{}) error
WriteMessage(msg interface{}) error
}
func ReadWireFormatVersion(conn net.Conn, exchangeTimeout time.Duration) (ver int, err error) {
var buf1 [1]byte
err = conn.SetReadDeadline(time.Now().Add(exchangeTimeout))
if err != nil {
panic(fmt.Errorf("can't set deadline: %v", err))
}
_, err = conn.Read(buf1[:])
ver = int(buf1[0])
return
}
const ProtocolWireVersion = 0
// protocol0 handles version 0 of the wire format
type protocol0 struct {
buffer *bytes.Buffer
enc *json.Encoder
conn net.Conn
}
// NewProtocol0 creates and initialises a protocol with wire format version 0.
func NewProtocol0(conn net.Conn) Protocol {
buf := bytes.NewBuffer(make([]byte, 5000))
return &protocol0{
buffer: buf,
enc: json.NewEncoder(buf),
conn: conn}
}
// SetDeadline sets the deadline for the subsequent WriteMessage/ReadMessage exchange.
func (c *protocol0) SetDeadline(t time.Time) {
err := c.conn.SetDeadline(t)
if err != nil {
panic(fmt.Errorf("can't set deadline: %v", err))
}
}
// ReadMessage reads from the connection one message with a JSON body
// preceded by its big-endian uint16 length.
func (c *protocol0) ReadMessage(msg interface{}) error {
c.buffer.Reset()
_, err := io.CopyN(c.buffer, c.conn, 2)
if err != nil {
return err
}
length := binary.BigEndian.Uint16(c.buffer.Bytes())
c.buffer.Reset()
_, err = io.CopyN(c.buffer, c.conn, int64(length))
if err != nil {
return err
}
return json.Unmarshal(c.buffer.Bytes(), msg)
}
// WriteMessage writes one message to the connection with a JSON body
// preceding it with its big-endian uint16 length.
func (c *protocol0) WriteMessage(msg interface{}) error {
c.buffer.Reset()
c.buffer.WriteString("\x00\x00") // placeholder for length
err := c.enc.Encode(msg)
if err != nil {
panic(fmt.Errorf("WriteMessage got: %v", err))
}
msgLen := c.buffer.Len() - 3 // length space, extra newline
toWrite := c.buffer.Bytes()
binary.BigEndian.PutUint16(toWrite[:2], uint16(msgLen))
_, err = c.conn.Write(toWrite[:msgLen+2])
return err
}
ubuntu-push-0.2+14.04.20140411/protocol/state-diag-client.gv 0000644 0000153 0177776 00000001216 12322032412 023730 0 ustar pbuser nogroup 0000000 0000000 digraph state_diagram_client {
label = "State diagram for client";
size="12,6";
rankdir=LR;
node [shape = doublecircle]; pingTimeout;
node [shape = circle];
start1 -> start2 [ label = "Write wire version" ];
start2 -> start3 [ label = "Write CONNECT" ];
start3 -> loop [ label = "Read CONNACK" ];
loop -> pong [ label = "Read PING" ];
loop -> broadcast [label = "Read BROADCAST"];
pong -> loop [label = "Write PONG"];
broadcast -> loop [label = "Write ACK"];
loop -> pingTimeout [
label = "Elapsed ping interval + exchange interval"];
}
ubuntu-push-0.2+14.04.20140411/protocol/messages.go 0000644 0000153 0177776 00000006165 12322032421 022242 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package protocol
// Structs representing messages.
import (
"encoding/json"
)
// System channel id using a shortened hex-encoded form for the NIL UUID.
const SystemChannelId = "0"
// CONNECT message
type ConnectMsg struct {
Type string `json:"T"`
ClientVer string
DeviceId string
Authorization string
Info map[string]interface{} `json:",omitempty"` // platform etc...
// maps channel ids (hex encoded UUIDs) to known client channel levels
Levels map[string]int64
}
// CONNACK message
type ConnAckMsg struct {
Type string `json:"T"`
Params ConnAckParams
}
// ConnAckParams carries the connection parameters from the server on
// connection acknowledgement.
type ConnAckParams struct {
// ping interval formatted time.Duration
PingInterval string
}
// SplittableMsg are messages that may require and are capable of splitting.
type SplittableMsg interface {
Split() (done bool)
}
// CONNBROKEN message, server side is breaking the connection for reason.
type ConnBrokenMsg struct {
Type string `json:"T"`
// reason
Reason string
}
func (m *ConnBrokenMsg) Split() bool {
return true
}
// CONNBROKEN reasons
const (
BrokenHostMismatch = "host-mismatch"
)
// PING/PONG messages
type PingPongMsg struct {
Type string `json:"T"`
}
const maxPayloadSize = 62 * 1024
// BROADCAST messages
type BroadcastMsg struct {
Type string `json:"T"`
AppId string `json:",omitempty"`
ChanId string
TopLevel int64
Payloads []json.RawMessage
splitting int
}
func (m *BroadcastMsg) Split() bool {
var prevTop int64
if m.splitting == 0 {
prevTop = m.TopLevel - int64(len(m.Payloads))
} else {
prevTop = m.TopLevel
m.Payloads = m.Payloads[len(m.Payloads):m.splitting]
m.TopLevel = prevTop + int64(len(m.Payloads))
}
payloads := m.Payloads
var size int
for i := range payloads {
size += len(payloads[i])
if size > maxPayloadSize {
m.TopLevel = prevTop + int64(i)
m.splitting = len(payloads)
m.Payloads = payloads[:i]
return false
}
}
return true
}
// Reset resets the splitting state if the message storage is to be
// reused.
func (b *BroadcastMsg) Reset() {
b.splitting = 0
}
// NOTIFICATIONS message
type NotificationsMsg struct {
Type string `json:"T"`
Notifications []Notification
}
// A single unicast notification
type Notification struct {
AppId string `json:"A"`
MsgId string `json:"M"`
// payload
Payload json.RawMessage `json:"P"`
}
// ACKnowledgement message
type AckMsg struct {
Type string `json:"T"`
}
// xxx ... query levels messages
ubuntu-push-0.2+14.04.20140411/protocol/messages_test.go 0000644 0000153 0177776 00000005414 12322032421 023275 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package protocol
import (
"encoding/json"
"fmt"
"strings"
. "launchpad.net/gocheck"
)
type messagesSuite struct{}
var _ = Suite(&messagesSuite{})
func (s *messagesSuite) TestSplitBroadcastMsgNop(c *C) {
b := &BroadcastMsg{
Type: "broadcast",
AppId: "APP",
ChanId: "0",
TopLevel: 2,
Payloads: []json.RawMessage{json.RawMessage(`{b:1}`), json.RawMessage(`{b:2}`)},
}
done := b.Split()
c.Check(done, Equals, true)
c.Check(b.TopLevel, Equals, int64(2))
c.Check(cap(b.Payloads), Equals, 2)
c.Check(len(b.Payloads), Equals, 2)
}
var payloadFmt = fmt.Sprintf(`{"b":%%d,"bloat":"%s"}`, strings.Repeat("x", 1024*2))
func manyParts(c int) []json.RawMessage {
payloads := make([]json.RawMessage, 0, 1)
for i := 0; i < c; i++ {
payloads = append(payloads, json.RawMessage(fmt.Sprintf(payloadFmt, i)))
}
return payloads
}
func (s *messagesSuite) TestSplitBroadcastMsgManyParts(c *C) {
payloads := manyParts(33)
n := len(payloads)
// more interesting this way
c.Assert(cap(payloads), Not(Equals), n)
b := &BroadcastMsg{
Type: "broadcast",
AppId: "APP",
ChanId: "0",
TopLevel: 500,
Payloads: payloads,
}
done := b.Split()
c.Assert(done, Equals, false)
n1 := len(b.Payloads)
c.Check(b.TopLevel, Equals, int64(500-n+n1))
buf, err := json.Marshal(b)
c.Assert(err, IsNil)
c.Assert(len(buf) <= 65535, Equals, true)
c.Check(len(buf)+len(payloads[n1]) > maxPayloadSize, Equals, true)
done = b.Split()
c.Assert(done, Equals, true)
n2 := len(b.Payloads)
c.Check(b.TopLevel, Equals, int64(500))
c.Check(n1+n2, Equals, n)
payloads = manyParts(61)
n = len(payloads)
b = &BroadcastMsg{
Type: "broadcast",
AppId: "APP",
ChanId: "0",
TopLevel: int64(n),
Payloads: payloads,
}
done = b.Split()
c.Assert(done, Equals, false)
n1 = len(b.Payloads)
done = b.Split()
c.Assert(done, Equals, false)
n2 = len(b.Payloads)
done = b.Split()
c.Assert(done, Equals, true)
n3 := len(b.Payloads)
c.Check(b.TopLevel, Equals, int64(n))
c.Check(n1+n2+n3, Equals, n)
// reset
b.Reset()
c.Check(b.splitting, Equals, 0)
}
func (s *messagesSuite) TestSplitConnBrokenMsg(c *C) {
c.Check((&ConnBrokenMsg{}).Split(), Equals, true)
}
ubuntu-push-0.2+14.04.20140411/protocol/protocol_test.go 0000644 0000153 0177776 00000014107 12322032412 023326 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package protocol
import (
"encoding/binary"
"encoding/json"
"io"
"net"
"testing"
"time"
. "launchpad.net/gocheck"
)
func TestProtocol(t *testing.T) { TestingT(t) }
type protocolSuite struct{}
var _ = Suite(&protocolSuite{})
type deadline struct {
kind string
deadAfter time.Duration
}
func (d *deadline) setDeadAfter(t time.Time) {
deadAfter := t.Sub(time.Now())
d.deadAfter = (deadAfter + time.Millisecond/2) / time.Millisecond * time.Millisecond
}
type rw struct {
buf []byte
n int
err error
}
type testConn struct {
deadlines []*deadline
reads []rw
writes []*rw
}
func (tc *testConn) LocalAddr() net.Addr {
return nil
}
func (tc *testConn) RemoteAddr() net.Addr {
return nil
}
func (tc *testConn) Close() error {
return nil
}
func (tc *testConn) SetDeadline(t time.Time) error {
deadline := tc.deadlines[0]
deadline.kind = "both"
deadline.setDeadAfter(t)
tc.deadlines = tc.deadlines[1:]
return nil
}
func (tc *testConn) SetReadDeadline(t time.Time) error {
deadline := tc.deadlines[0]
deadline.kind = "read"
deadline.setDeadAfter(t)
tc.deadlines = tc.deadlines[1:]
return nil
}
func (tc *testConn) SetWriteDeadline(t time.Time) error {
deadline := tc.deadlines[0]
deadline.kind = "write"
deadline.setDeadAfter(t)
tc.deadlines = tc.deadlines[1:]
return nil
}
func (tc *testConn) Read(buf []byte) (n int, err error) {
read := tc.reads[0]
copy(buf, read.buf)
tc.reads = tc.reads[1:]
return read.n, read.err
}
func (tc *testConn) Write(buf []byte) (n int, err error) {
write := tc.writes[0]
n = copy(write.buf, buf)
write.buf = write.buf[:n]
write.n = n
err = write.err
tc.writes = tc.writes[1:]
return
}
func (s *protocolSuite) TestReadWireFormatVersion(c *C) {
deadl := deadline{}
read1 := rw{buf: []byte{42}, n: 1}
tc := &testConn{reads: []rw{read1}, deadlines: []*deadline{&deadl}}
ver, err := ReadWireFormatVersion(tc, time.Minute)
c.Check(err, IsNil)
c.Check(ver, Equals, 42)
c.Check(deadl.kind, Equals, "read")
c.Check(deadl.deadAfter, Equals, time.Minute)
}
func (s *protocolSuite) TestReadWireFormatVersionError(c *C) {
deadl := deadline{}
read1 := rw{err: io.EOF}
tc := &testConn{reads: []rw{read1}, deadlines: []*deadline{&deadl}}
_, err := ReadWireFormatVersion(tc, time.Minute)
c.Check(err, Equals, io.EOF)
}
func (s *protocolSuite) TestSetDeadline(c *C) {
deadl := deadline{}
tc := &testConn{deadlines: []*deadline{&deadl}}
pc := NewProtocol0(tc)
pc.SetDeadline(time.Now().Add(time.Minute))
c.Check(deadl.kind, Equals, "both")
c.Check(deadl.deadAfter, Equals, time.Minute)
}
type testMsg struct {
Type string `json:"T"`
A uint64
}
func lengthAsBytes(length uint16) []byte {
var buf [2]byte
var res = buf[:]
binary.BigEndian.PutUint16(res, length)
return res
}
func (s *protocolSuite) TestReadMessage(c *C) {
msgBuf, _ := json.Marshal(testMsg{Type: "msg", A: 2000})
readMsgLen := rw{buf: lengthAsBytes(uint16(len(msgBuf))), n: 2}
readMsgBody := rw{buf: msgBuf, n: len(msgBuf)}
tc := &testConn{reads: []rw{readMsgLen, readMsgBody}}
pc := NewProtocol0(tc)
var recvMsg testMsg
err := pc.ReadMessage(&recvMsg)
c.Check(err, IsNil)
c.Check(recvMsg, DeepEquals, testMsg{Type: "msg", A: 2000})
}
func (s *protocolSuite) TestReadMessageBits(c *C) {
msgBuf, _ := json.Marshal(testMsg{Type: "msg", A: 2000})
readMsgLen := rw{buf: lengthAsBytes(uint16(len(msgBuf))), n: 2}
readMsgBody1 := rw{buf: msgBuf[:5], n: 5}
readMsgBody2 := rw{buf: msgBuf[5:], n: len(msgBuf) - 5}
tc := &testConn{reads: []rw{readMsgLen, readMsgBody1, readMsgBody2}}
pc := NewProtocol0(tc)
var recvMsg testMsg
err := pc.ReadMessage(&recvMsg)
c.Check(err, IsNil)
c.Check(recvMsg, DeepEquals, testMsg{Type: "msg", A: 2000})
}
func (s *protocolSuite) TestReadMessageIOErrors(c *C) {
msgBuf, _ := json.Marshal(testMsg{Type: "msg", A: 2000})
readMsgLenErr := rw{n: 1, err: io.ErrClosedPipe}
tc1 := &testConn{reads: []rw{readMsgLenErr}}
pc1 := NewProtocol0(tc1)
var recvMsg testMsg
err := pc1.ReadMessage(&recvMsg)
c.Check(err, Equals, io.ErrClosedPipe)
readMsgLen := rw{buf: lengthAsBytes(uint16(len(msgBuf))), n: 2}
readMsgBody1 := rw{buf: msgBuf[:5], n: 5}
readMsgBody2Err := rw{n: 2, err: io.EOF}
tc2 := &testConn{reads: []rw{readMsgLen, readMsgBody1, readMsgBody2Err}}
pc2 := NewProtocol0(tc2)
err = pc2.ReadMessage(&recvMsg)
c.Check(err, Equals, io.EOF)
}
func (s *protocolSuite) TestReadMessageBrokenJSON(c *C) {
msgBuf := []byte("{\"T\"}")
readMsgLen := rw{buf: lengthAsBytes(uint16(len(msgBuf))), n: 2}
readMsgBody := rw{buf: msgBuf, n: len(msgBuf)}
tc := &testConn{reads: []rw{readMsgLen, readMsgBody}}
pc := NewProtocol0(tc)
var recvMsg testMsg
err := pc.ReadMessage(&recvMsg)
c.Check(err, FitsTypeOf, &json.SyntaxError{})
}
func (s *protocolSuite) TestWriteMessage(c *C) {
writeMsg := rw{buf: make([]byte, 64)}
tc := &testConn{writes: []*rw{&writeMsg}}
pc := NewProtocol0(tc)
msg := testMsg{Type: "m", A: 9999}
err := pc.WriteMessage(&msg)
c.Check(err, IsNil)
var msgLen int = int(binary.BigEndian.Uint16(writeMsg.buf[:2]))
c.Check(msgLen, Equals, len(writeMsg.buf)-2)
var wroteMsg testMsg
formatErr := json.Unmarshal(writeMsg.buf[2:], &wroteMsg)
c.Check(formatErr, IsNil)
c.Check(wroteMsg, DeepEquals, testMsg{Type: "m", A: 9999})
}
func (s *protocolSuite) TestWriteMessageIOErrors(c *C) {
writeMsgErr := rw{buf: make([]byte, 0), err: io.ErrClosedPipe}
tc1 := &testConn{writes: []*rw{&writeMsgErr}}
pc1 := NewProtocol0(tc1)
msg := testMsg{Type: "m", A: 9999}
err := pc1.WriteMessage(&msg)
c.Check(err, Equals, io.ErrClosedPipe)
}
ubuntu-push-0.2+14.04.20140411/protocol/state-diag-session.svg 0000644 0000153 0177776 00000022615 12322032412 024326 0 ustar pbuser nogroup 0000000 0000000
state_diagram_session
State diagram for session
start1
start1
start2
start2
start1->start2
Read wire version
start3
start3
start2->start3
Read CONNECT
loop
loop
start3->loop
Write CONNACK
ping
ping
loop->ping
Elapsed ping interval
broadcast
broadcast
loop->broadcast
Receive broadcast request
pong_wait
pong_wait
ping->pong_wait
Write PING
ack_wait
ack_wait
broadcast->ack_wait
Write BROADCAST [fits one wire msg]
split_broadcast
split_broadcast
broadcast->split_broadcast
BROADCAST does not fit one wire msg
pong_wait->loop
Read PONG
ack_wait->loop
Read ACK
split_broadcast->loop
All split msgs written
split_ack_wait
split_ack_wait
split_broadcast->split_ack_wait
Write split BROADCAST
split_ack_wait->split_broadcast
Read ACK
ubuntu-push-0.2+14.04.20140411/protocol/state-diag-client.svg 0000644 0000153 0177776 00000014416 12322032412 024121 0 ustar pbuser nogroup 0000000 0000000
state_diagram_client
State diagram for client
pingTimeout
pingTimeout
start1
start1
start2
start2
start1->start2
Write wire version
start3
start3
start2->start3
Write CONNECT
loop
loop
start3->loop
Read CONNACK
loop->pingTimeout
Elapsed ping interval + exchange interval
pong
pong
loop->pong
Read PING
broadcast
broadcast
loop->broadcast
Read BROADCAST
pong->loop
Write PONG
broadcast->loop
Write ACK
ubuntu-push-0.2+14.04.20140411/client/ 0000755 0000153 0177776 00000000000 12322032700 017511 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/client/client.dot 0000644 0000153 0177776 00000005051 12322032412 021500 0 ustar pbuser nogroup 0000000 0000000 digraph g {
// both "neato" and "dot" produce reasonable & interesting outputs
graph [rankdir=LR size="20,15" overlap="scale" splines="true"]
node [fontname="Ubuntu Mono" fontsize=12 margin=0]
edge [fontname="Ubuntu Mono" fontsize=10 decorate=true]
// states
node []
"INIT" [shape=doublecircle]
"Error" [shape=doublecircle color="#990000"]
"Configured"
"Identified"
"SysBusOK" [label="System\nBus\nGO"]
"Waiting4Conn" [label="Waiting\nfor\nConnectivity"]
"Connected" [label="Network\nConnected"]
"SesBusOK" [label="Session\nBus\nGO"]
"Running"
"Notified"
"Shown" [label="Notification\nShown"]
"Clicked"
// auto-transitions
node [shape=triangle]
"read config" [label="read\nconfig"]
"get system id" [label="get\nsystem\nid"]
"conn sys bus" [label="connect\nsystem\nbus"]
"watch conn" [label="watch\nconnectivity"]
"conn ses bus" [label="connect\nsession\nbus"]
"start session" [label="start\npush\nsession"]
"show notification" [label="show\nnotification"]
"dispatch URL" [label="dispatch\nURL"]
//
"INIT" -> "read config"
"Configured" -> "get system id"
"Identified" -> { "conn sys bus" "conn ses bus" }
"SysBusOK" -> "watch conn"
"Connected" -> "start session"
"Notified" -> "show notification"
"Clicked" -> "dispatch URL" -> "SesBusOK"
"Shown" -> "SesBusOK" // XXX state:state auto-transition?
// events
edge [color="#000099"]
"Waiting4Conn" -> "Connected" [label="connected"]
"Waiting4Conn" -> "Waiting4Conn" [label="disconnected"]
"SesBusOK" -> "Notified" [label="notification\narrived"]
"SesBusOK" -> "Clicked" [label="user\nclicked\nnotification"]
{ "Connected" "Running" } -> "Waiting4Conn" [constraint=false label="disconnected"]
"Running" -> "SesBusOK" [constraint=false style=dotted label=notification]
"Shown" -> "Running" [constraint=false style=dotted label=shown]
// OKs
edge [color="#009900" label="OK"]
"read config" -> "Configured"
"get system id" -> "Identified"
"conn sys bus" -> "SysBusOK"
"conn ses bus" -> "SesBusOK"
"watch conn" -> "Waiting4Conn"
"start session" -> "Running"
"show notification" -> "Shown"
//err
edge [color="#990000" label="err" constraint=false]
{ "read config"
"get system id"
} -> Error
"conn ses bus" -> "conn ses bus"
"conn sys bus" -> "conn sys bus"
"watch conn" -> "conn sys bus"
"start session" -> "start session"
}
ubuntu-push-0.2+14.04.20140411/client/client.go 0000644 0000153 0177776 00000026313 12322032421 021323 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package client implements the Ubuntu Push Notifications client-side
// daemon.
package client
import (
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"launchpad.net/go-dbus/v1"
"launchpad.net/ubuntu-push/bus"
"launchpad.net/ubuntu-push/bus/connectivity"
"launchpad.net/ubuntu-push/bus/networkmanager"
"launchpad.net/ubuntu-push/bus/notifications"
"launchpad.net/ubuntu-push/bus/systemimage"
"launchpad.net/ubuntu-push/bus/urldispatcher"
"launchpad.net/ubuntu-push/client/session"
"launchpad.net/ubuntu-push/client/session/levelmap"
"launchpad.net/ubuntu-push/config"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/util"
"launchpad.net/ubuntu-push/whoopsie/identifier"
)
// ClientConfig holds the client configuration
type ClientConfig struct {
connectivity.ConnectivityConfig // q.v.
// A reasonably large timeout for receive/answer pairs
ExchangeTimeout config.ConfigTimeDuration `json:"exchange_timeout"`
// A timeout to use when trying to connect to the server
ConnectTimeout config.ConfigTimeDuration `json:"connect_timeout"`
// The server to connect to or url to query for hosts to connect to
Addr string
// Host list management
HostsCachingExpiryTime config.ConfigTimeDuration `json:"hosts_cache_expiry"` // potentially refresh host list after
ExpectAllRepairedTime config.ConfigTimeDuration `json:"expect_all_repaired"` // worth retrying all servers after
// The PEM-encoded server certificate
CertPEMFile string `json:"cert_pem_file"`
// The logging level (one of "debug", "info", "error")
LogLevel string `json:"log_level"`
}
// PushClient is the Ubuntu Push Notifications client-side daemon.
type PushClient struct {
leveldbPath string
configPath string
config ClientConfig
log logger.Logger
pem []byte
idder identifier.Id
deviceId string
notificationsEndp bus.Endpoint
urlDispatcherEndp bus.Endpoint
connectivityEndp bus.Endpoint
systemImageEndp bus.Endpoint
systemImageInfo *systemimage.InfoResult
connCh chan bool
hasConnectivity bool
actionsCh <-chan notifications.RawActionReply
session *session.ClientSession
sessionConnectedCh chan uint32
}
var ACTION_ID_SNOWFLAKE = "::ubuntu-push-client::"
// Creates a new Ubuntu Push Notifications client-side daemon that will use
// the given configuration file.
func NewPushClient(configPath string, leveldbPath string) *PushClient {
client := new(PushClient)
client.configPath = configPath
client.leveldbPath = leveldbPath
return client
}
// configure loads its configuration, and sets it up.
func (client *PushClient) configure() error {
f, err := os.Open(client.configPath)
if err != nil {
return fmt.Errorf("opening config: %v", err)
}
err = config.ReadConfig(f, &client.config)
if err != nil {
return fmt.Errorf("reading config: %v", err)
}
// ignore spaces
client.config.Addr = strings.Replace(client.config.Addr, " ", "", -1)
if client.config.Addr == "" {
return errors.New("no hosts specified")
}
// later, we'll be specifying more logging options in the config file
client.log = logger.NewSimpleLogger(os.Stderr, client.config.LogLevel)
// overridden for testing
client.idder = identifier.New()
client.notificationsEndp = bus.SessionBus.Endpoint(notifications.BusAddress, client.log)
client.urlDispatcherEndp = bus.SessionBus.Endpoint(urldispatcher.BusAddress, client.log)
client.connectivityEndp = bus.SystemBus.Endpoint(networkmanager.BusAddress, client.log)
client.systemImageEndp = bus.SystemBus.Endpoint(systemimage.BusAddress, client.log)
client.connCh = make(chan bool, 1)
client.sessionConnectedCh = make(chan uint32, 1)
if client.config.CertPEMFile != "" {
client.pem, err = ioutil.ReadFile(client.config.CertPEMFile)
if err != nil {
return fmt.Errorf("reading PEM file: %v", err)
}
// sanity check
p, _ := pem.Decode(client.pem)
if p == nil {
return fmt.Errorf("no PEM found in PEM file")
}
}
return nil
}
// deriveSessionConfig dervies the session configuration from the client configuration bits.
func (client *PushClient) deriveSessionConfig(info map[string]interface{}) session.ClientSessionConfig {
return session.ClientSessionConfig{
ConnectTimeout: client.config.ConnectTimeout.TimeDuration(),
ExchangeTimeout: client.config.ExchangeTimeout.TimeDuration(),
HostsCachingExpiryTime: client.config.HostsCachingExpiryTime.TimeDuration(),
ExpectAllRepairedTime: client.config.ExpectAllRepairedTime.TimeDuration(),
PEM: client.pem,
Info: info,
}
}
// getDeviceId gets the whoopsie identifier for the device
func (client *PushClient) getDeviceId() error {
err := client.idder.Generate()
if err != nil {
return err
}
client.deviceId = client.idder.String()
return nil
}
// takeTheBus starts the connection(s) to D-Bus and sets up associated event channels
func (client *PushClient) takeTheBus() error {
go connectivity.ConnectedState(client.connectivityEndp,
client.config.ConnectivityConfig, client.log, client.connCh)
iniCh := make(chan uint32)
go func() { iniCh <- util.NewAutoRedialer(client.notificationsEndp).Redial() }()
go func() { iniCh <- util.NewAutoRedialer(client.urlDispatcherEndp).Redial() }()
go func() { iniCh <- util.NewAutoRedialer(client.systemImageEndp).Redial() }()
<-iniCh
<-iniCh
<-iniCh
sysimg := systemimage.New(client.systemImageEndp, client.log)
info, err := sysimg.Info()
if err != nil {
return err
}
client.systemImageInfo = info
actionsCh, err := notifications.Raw(client.notificationsEndp, client.log).WatchActions()
client.actionsCh = actionsCh
return err
}
// initSession creates the session object
func (client *PushClient) initSession() error {
info := map[string]interface{}{
"device": client.systemImageInfo.Device,
"channel": client.systemImageInfo.Channel,
"build_number": client.systemImageInfo.BuildNumber,
}
sess, err := session.NewSession(client.config.Addr,
client.deriveSessionConfig(info), client.deviceId,
client.levelMapFactory, client.log)
if err != nil {
return err
}
client.session = sess
return nil
}
// levelmapFactory returns a levelMap for the session
func (client *PushClient) levelMapFactory() (levelmap.LevelMap, error) {
if client.leveldbPath == "" {
return levelmap.NewLevelMap()
} else {
return levelmap.NewSqliteLevelMap(client.leveldbPath)
}
}
// handleConnState deals with connectivity events
func (client *PushClient) handleConnState(hasConnectivity bool) {
if client.hasConnectivity == hasConnectivity {
// nothing to do!
return
}
client.hasConnectivity = hasConnectivity
if hasConnectivity {
client.session.AutoRedial(client.sessionConnectedCh)
} else {
client.session.Close()
}
}
// handleErr deals with the session erroring out of its loop
func (client *PushClient) handleErr(err error) {
// if we're not connected, we don't really care
client.log.Errorf("session exited: %s", err)
if client.hasConnectivity {
client.session.AutoRedial(client.sessionConnectedCh)
}
}
// filterNotification finds out if the notification is about an actual
// upgrade for the device. It expects msg.Decoded entries to look
// like:
//
// {
// "IMAGE-CHANNEL/DEVICE-MODEL": [BUILD-NUMBER, CHANNEL-ALIAS]
// ...
// }
func (client *PushClient) filterNotification(msg *session.Notification) bool {
n := len(msg.Decoded)
if n == 0 {
return false
}
// they are all for us, consider last
last := msg.Decoded[n-1]
tag := fmt.Sprintf("%s/%s", client.systemImageInfo.Channel, client.systemImageInfo.Device)
entry, ok := last[tag]
if !ok {
return false
}
pair, ok := entry.([]interface{})
if !ok {
return false
}
if len(pair) < 1 {
return false
}
buildNumber, ok := pair[0].(float64)
if !ok {
return false
}
curBuildNumber := float64(client.systemImageInfo.BuildNumber)
if buildNumber > curBuildNumber {
return true
}
// xxx we should really compare channel_target and alias here
// going backward by a margin, assume switch of target
if buildNumber < curBuildNumber && (curBuildNumber-buildNumber) > 10 {
return true
}
return false
}
// handleNotification deals with receiving a notification
func (client *PushClient) handleNotification(msg *session.Notification) error {
if !client.filterNotification(msg) {
return nil
}
action_id := ACTION_ID_SNOWFLAKE
a := []string{action_id, "Go get it!"} // action value not visible on the phone
h := map[string]*dbus.Variant{"x-canonical-switch-to-application": &dbus.Variant{true}}
nots := notifications.Raw(client.notificationsEndp, client.log)
body := "Tap to open the system updater."
if msg != nil {
body = fmt.Sprintf("[%d] %s", msg.TopLevel, body)
}
not_id, err := nots.Notify(
"ubuntu-push-client", // app name
uint32(0), // id
"update_manager_icon", // icon
"There's an updated system image.", // summary
body, // body
a, // actions
h, // hints
int32(10*1000), // timeout (ms)
)
if err != nil {
client.log.Errorf("showing notification: %s", err)
return err
}
client.log.Debugf("got notification id %d", not_id)
return nil
}
// handleClick deals with the user clicking a notification
func (client *PushClient) handleClick(action_id string) error {
if action_id != ACTION_ID_SNOWFLAKE {
return nil
}
// it doesn't get much simpler...
urld := urldispatcher.New(client.urlDispatcherEndp, client.log)
return urld.DispatchURL("settings:///system/system-update")
}
// doLoop connects events with their handlers
func (client *PushClient) doLoop(connhandler func(bool), clickhandler func(string) error, notifhandler func(*session.Notification) error, errhandler func(error)) {
for {
select {
case state := <-client.connCh:
connhandler(state)
case action := <-client.actionsCh:
clickhandler(action.ActionId)
case msg := <-client.session.MsgCh:
notifhandler(msg)
case err := <-client.session.ErrCh:
errhandler(err)
case count := <-client.sessionConnectedCh:
client.log.Debugf("Session connected after %d attempts", count)
}
}
}
// doStart calls each of its arguments in order, returning the first non-nil
// error (or nil at the end)
func (client *PushClient) doStart(fs ...func() error) error {
for _, f := range fs {
if err := f(); err != nil {
return err
}
}
return nil
}
// Loop calls doLoop with the "real" handlers
func (client *PushClient) Loop() {
client.doLoop(client.handleConnState, client.handleClick,
client.handleNotification, client.handleErr)
}
// Start calls doStart with the "real" starters
func (client *PushClient) Start() error {
return client.doStart(
client.configure,
client.getDeviceId,
client.takeTheBus,
client.initSession,
)
}
ubuntu-push-0.2+14.04.20140411/client/client_test.go 0000644 0000153 0177776 00000061720 12322032421 022363 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package client
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"path/filepath"
"reflect"
"testing"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/bus"
"launchpad.net/ubuntu-push/bus/networkmanager"
"launchpad.net/ubuntu-push/bus/notifications"
"launchpad.net/ubuntu-push/bus/systemimage"
testibus "launchpad.net/ubuntu-push/bus/testing"
"launchpad.net/ubuntu-push/client/session"
"launchpad.net/ubuntu-push/client/session/levelmap"
helpers "launchpad.net/ubuntu-push/testing"
"launchpad.net/ubuntu-push/testing/condition"
"launchpad.net/ubuntu-push/util"
"launchpad.net/ubuntu-push/whoopsie/identifier"
idtesting "launchpad.net/ubuntu-push/whoopsie/identifier/testing"
)
func TestClient(t *testing.T) { TestingT(t) }
// takeNext takes a value from given channel with a 5s timeout
func takeNextBool(ch <-chan bool) bool {
select {
case <-time.After(5 * time.Second):
panic("channel stuck: too long waiting")
case v := <-ch:
return v
}
}
type clientSuite struct {
timeouts []time.Duration
configPath string
leveldbPath string
log *helpers.TestLogger
}
var _ = Suite(&clientSuite{})
const (
staticText = "something ipsum dolor something"
staticHash = "6155f83b471583f47c99998a472a178f"
)
func mkHandler(text string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.(http.Flusher).Flush()
w.Write([]byte(text))
w.(http.Flusher).Flush()
}
}
func (cs *clientSuite) SetUpSuite(c *C) {
cs.timeouts = util.SwapTimeouts([]time.Duration{0})
cs.leveldbPath = ""
}
func (cs *clientSuite) TearDownSuite(c *C) {
util.SwapTimeouts(cs.timeouts)
cs.timeouts = nil
}
func (cs *clientSuite) writeTestConfig(overrides map[string]interface{}) {
pem_file := helpers.SourceRelative("../server/acceptance/ssl/testing.cert")
cfgMap := map[string]interface{}{
"connect_timeout": "7ms",
"exchange_timeout": "10ms",
"hosts_cache_expiry": "1h",
"expect_all_repaired": "30m",
"stabilizing_timeout": "0ms",
"connectivity_check_url": "",
"connectivity_check_md5": "",
"addr": ":0",
"cert_pem_file": pem_file,
"recheck_timeout": "3h",
"log_level": "debug",
}
for k, v := range overrides {
cfgMap[k] = v
}
cfgBlob, err := json.Marshal(cfgMap)
if err != nil {
panic(err)
}
ioutil.WriteFile(cs.configPath, cfgBlob, 0600)
}
func (cs *clientSuite) SetUpTest(c *C) {
cs.log = helpers.NewTestLogger(c, "debug")
dir := c.MkDir()
cs.configPath = filepath.Join(dir, "config")
cs.writeTestConfig(nil)
}
type sqlientSuite struct{ clientSuite }
func (s *sqlientSuite) SetUpSuite(c *C) {
s.clientSuite.SetUpSuite(c)
s.leveldbPath = ":memory:"
}
var _ = Suite(&sqlientSuite{})
/*****************************************************************
configure tests
******************************************************************/
func (cs *clientSuite) TestConfigureWorks(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
err := cli.configure()
c.Assert(err, IsNil)
c.Assert(cli.config, NotNil)
c.Check(cli.config.ExchangeTimeout.TimeDuration(), Equals, time.Duration(10*time.Millisecond))
}
func (cs *clientSuite) TestConfigureSetsUpLog(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
c.Check(cli.log, IsNil)
err := cli.configure()
c.Assert(err, IsNil)
c.Assert(cli.log, NotNil)
}
func (cs *clientSuite) TestConfigureSetsUpPEM(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
c.Check(cli.pem, IsNil)
err := cli.configure()
c.Assert(err, IsNil)
c.Assert(cli.pem, NotNil)
}
func (cs *clientSuite) TestConfigureSetsUpIdder(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
c.Check(cli.idder, IsNil)
err := cli.configure()
c.Assert(err, IsNil)
c.Assert(cli.idder, DeepEquals, identifier.New())
}
func (cs *clientSuite) TestConfigureSetsUpEndpoints(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
c.Check(cli.notificationsEndp, IsNil)
c.Check(cli.urlDispatcherEndp, IsNil)
c.Check(cli.connectivityEndp, IsNil)
err := cli.configure()
c.Assert(err, IsNil)
c.Assert(cli.notificationsEndp, NotNil)
c.Assert(cli.urlDispatcherEndp, NotNil)
c.Assert(cli.connectivityEndp, NotNil)
}
func (cs *clientSuite) TestConfigureSetsUpConnCh(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
c.Check(cli.connCh, IsNil)
err := cli.configure()
c.Assert(err, IsNil)
c.Assert(cli.connCh, NotNil)
}
func (cs *clientSuite) TestConfigureBailsOnBadFilename(c *C) {
cli := NewPushClient("/does/not/exist", cs.leveldbPath)
err := cli.configure()
c.Assert(err, NotNil)
}
func (cs *clientSuite) TestConfigureBailsOnBadConfig(c *C) {
cli := NewPushClient("/etc/passwd", cs.leveldbPath)
err := cli.configure()
c.Assert(err, NotNil)
}
func (cs *clientSuite) TestConfigureBailsOnBadPEMFilename(c *C) {
cs.writeTestConfig(map[string]interface{}{
"cert_pem_file": "/a/b/c",
})
cli := NewPushClient(cs.configPath, cs.leveldbPath)
err := cli.configure()
c.Assert(err, ErrorMatches, "reading PEM file: .*")
}
func (cs *clientSuite) TestConfigureBailsOnBadPEM(c *C) {
cs.writeTestConfig(map[string]interface{}{
"cert_pem_file": "/etc/passwd",
})
cli := NewPushClient(cs.configPath, cs.leveldbPath)
err := cli.configure()
c.Assert(err, ErrorMatches, "no PEM found.*")
}
func (cs *clientSuite) TestConfigureBailsOnNoHosts(c *C) {
cs.writeTestConfig(map[string]interface{}{
"addr": " ",
})
cli := NewPushClient(cs.configPath, cs.leveldbPath)
err := cli.configure()
c.Assert(err, ErrorMatches, "no hosts specified")
}
func (cs *clientSuite) TestConfigureRemovesBlanksInAddr(c *C) {
cs.writeTestConfig(map[string]interface{}{
"addr": " foo: 443",
})
cli := NewPushClient(cs.configPath, cs.leveldbPath)
err := cli.configure()
c.Assert(err, IsNil)
c.Check(cli.config.Addr, Equals, "foo:443")
}
/*****************************************************************
deriveSessionConfig tests
******************************************************************/
func (cs *clientSuite) TestDeriveSessionConfig(c *C) {
info := map[string]interface{}{
"foo": 1,
}
cli := NewPushClient(cs.configPath, cs.leveldbPath)
err := cli.configure()
c.Assert(err, IsNil)
expected := session.ClientSessionConfig{
ConnectTimeout: 7 * time.Millisecond,
ExchangeTimeout: 10 * time.Millisecond,
HostsCachingExpiryTime: 1 * time.Hour,
ExpectAllRepairedTime: 30 * time.Minute,
PEM: cli.pem,
Info: info,
}
// sanity check that we are looking at all fields
vExpected := reflect.ValueOf(expected)
nf := vExpected.NumField()
for i := 0; i < nf; i++ {
fv := vExpected.Field(i)
// field isn't empty/zero
c.Assert(fv.Interface(), Not(DeepEquals), reflect.Zero(fv.Type()).Interface(), Commentf("forgot about: %s", vExpected.Type().Field(i).Name))
}
// finally compare
conf := cli.deriveSessionConfig(info)
c.Check(conf, DeepEquals, expected)
}
/*****************************************************************
getDeviceId tests
******************************************************************/
func (cs *clientSuite) TestGetDeviceIdWorks(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
cli.idder = identifier.New()
c.Check(cli.deviceId, Equals, "")
c.Check(cli.getDeviceId(), IsNil)
c.Check(cli.deviceId, HasLen, 128)
}
func (cs *clientSuite) TestGetDeviceIdCanFail(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
cli.idder = idtesting.Failing()
c.Check(cli.deviceId, Equals, "")
c.Check(cli.getDeviceId(), NotNil)
}
/*****************************************************************
takeTheBus tests
******************************************************************/
func (cs *clientSuite) TestTakeTheBusWorks(c *C) {
// http server used for connectivity test
ts := httptest.NewServer(mkHandler(staticText))
defer ts.Close()
// testing endpoints
nCond := condition.Fail2Work(3)
nEndp := testibus.NewMultiValuedTestingEndpoint(nCond, condition.Work(true),
[]interface{}{uint32(1), "hello"})
uCond := condition.Fail2Work(5)
uEndp := testibus.NewTestingEndpoint(uCond, condition.Work(false))
cCond := condition.Fail2Work(7)
cEndp := testibus.NewTestingEndpoint(cCond, condition.Work(true),
uint32(networkmanager.ConnectedGlobal),
)
siCond := condition.Fail2Work(2)
siEndp := testibus.NewMultiValuedTestingEndpoint(siCond, condition.Work(true), []interface{}{int32(101), "mako", "daily", "Unknown", map[string]string{}})
testibus.SetWatchTicker(cEndp, make(chan bool))
// ok, create the thing
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
err := cli.configure()
c.Assert(err, IsNil)
// the user actions channel has not been set up
c.Check(cli.actionsCh, IsNil)
// and stomp on things for testing
cli.config.ConnectivityConfig.ConnectivityCheckURL = ts.URL
cli.config.ConnectivityConfig.ConnectivityCheckMD5 = staticHash
cli.notificationsEndp = nEndp
cli.urlDispatcherEndp = uEndp
cli.connectivityEndp = cEndp
cli.systemImageEndp = siEndp
c.Assert(cli.takeTheBus(), IsNil)
// the notifications and urldispatcher endpoints retried until connected
c.Check(nCond.OK(), Equals, true)
c.Check(uCond.OK(), Equals, true)
// the user actions channel has now been set up
c.Check(cli.actionsCh, NotNil)
c.Check(takeNextBool(cli.connCh), Equals, false)
c.Check(takeNextBool(cli.connCh), Equals, true)
// the connectivity endpoint retried until connected
c.Check(cCond.OK(), Equals, true)
// the systemimage endpoint retried until connected
c.Check(siCond.OK(), Equals, true)
}
// takeTheBus can, in fact, fail
func (cs *clientSuite) TestTakeTheBusCanFail(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
err := cli.configure()
cli.log = cs.log
c.Assert(err, IsNil)
// the user actions channel has not been set up
c.Check(cli.actionsCh, IsNil)
// and stomp on things for testing
cli.notificationsEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false))
cli.urlDispatcherEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false))
cli.connectivityEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false))
cli.systemImageEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false))
c.Check(cli.takeTheBus(), NotNil)
c.Check(cli.actionsCh, IsNil)
}
/*****************************************************************
handleErr tests
******************************************************************/
func (cs *clientSuite) TestHandleErr(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
cli.systemImageInfo = siInfoRes
c.Assert(cli.initSession(), IsNil)
cs.log.ResetCapture()
cli.hasConnectivity = true
cli.handleErr(errors.New("bananas"))
c.Check(cs.log.Captured(), Matches, ".*session exited.*bananas\n")
}
/*****************************************************************
levelmapFactory tests
******************************************************************/
func (cs *clientSuite) TestLevelMapFactoryNoDbPath(c *C) {
cli := NewPushClient(cs.configPath, "")
ln, err := cli.levelMapFactory()
c.Assert(err, IsNil)
c.Check(fmt.Sprintf("%T", ln), Equals, "*levelmap.mapLevelMap")
}
func (cs *clientSuite) TestLevelMapFactoryWithDbPath(c *C) {
cli := NewPushClient(cs.configPath, ":memory:")
ln, err := cli.levelMapFactory()
c.Assert(err, IsNil)
c.Check(fmt.Sprintf("%T", ln), Equals, "*levelmap.sqliteLevelMap")
}
/*****************************************************************
handleConnState tests
******************************************************************/
func (cs *clientSuite) TestHandleConnStateD2C(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
cli.systemImageInfo = siInfoRes
c.Assert(cli.initSession(), IsNil)
c.Assert(cli.hasConnectivity, Equals, false)
cli.handleConnState(true)
c.Check(cli.hasConnectivity, Equals, true)
c.Assert(cli.session, NotNil)
}
func (cs *clientSuite) TestHandleConnStateSame(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
// here we want to check that we don't do anything
c.Assert(cli.session, IsNil)
c.Assert(cli.hasConnectivity, Equals, false)
cli.handleConnState(false)
c.Check(cli.session, IsNil)
cli.hasConnectivity = true
cli.handleConnState(true)
c.Check(cli.session, IsNil)
}
func (cs *clientSuite) TestHandleConnStateC2D(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
cli.session, _ = session.NewSession(cli.config.Addr, cli.deriveSessionConfig(nil), cli.deviceId, levelmap.NewLevelMap, cs.log)
cli.session.Dial()
cli.hasConnectivity = true
// cli.session.State() will be "Error" here, for now at least
c.Check(cli.session.State(), Not(Equals), session.Disconnected)
cli.handleConnState(false)
c.Check(cli.session.State(), Equals, session.Disconnected)
}
func (cs *clientSuite) TestHandleConnStateC2DPending(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
cli.session, _ = session.NewSession(cli.config.Addr, cli.deriveSessionConfig(nil), cli.deviceId, levelmap.NewLevelMap, cs.log)
cli.hasConnectivity = true
cli.handleConnState(false)
c.Check(cli.session.State(), Equals, session.Disconnected)
}
/*****************************************************************
filterNotification tests
******************************************************************/
var siInfoRes = &systemimage.InfoResult{
Device: "mako",
Channel: "daily",
BuildNumber: 102,
LastUpdate: "Unknown",
}
func (cs *clientSuite) TestFilterNotification(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.systemImageInfo = siInfoRes
// empty
msg := &session.Notification{}
c.Check(cli.filterNotification(msg), Equals, false)
// same build number
msg = &session.Notification{
Decoded: []map[string]interface{}{
map[string]interface{}{
"daily/mako": []interface{}{float64(102), "tubular"},
},
},
}
c.Check(cli.filterNotification(msg), Equals, false)
// higher build number and pick last
msg = &session.Notification{
Decoded: []map[string]interface{}{
map[string]interface{}{
"daily/mako": []interface{}{float64(102), "tubular"},
},
map[string]interface{}{
"daily/mako": []interface{}{float64(103), "tubular"},
},
},
}
c.Check(cli.filterNotification(msg), Equals, true)
// going backward by a margin, assume switch of alias
msg = &session.Notification{
Decoded: []map[string]interface{}{
map[string]interface{}{
"daily/mako": []interface{}{float64(102), "tubular"},
},
map[string]interface{}{
"daily/mako": []interface{}{float64(2), "urban"},
},
},
}
c.Check(cli.filterNotification(msg), Equals, true)
}
func (cs *clientSuite) TestFilterNotificationRobust(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.systemImageInfo = siInfoRes
msg := &session.Notification{
Decoded: []map[string]interface{}{
map[string]interface{}{},
},
}
c.Check(cli.filterNotification(msg), Equals, false)
for _, broken := range []interface{}{
5,
[]interface{}{},
[]interface{}{55},
} {
msg := &session.Notification{
Decoded: []map[string]interface{}{
map[string]interface{}{
"daily/mako": broken,
},
},
}
c.Check(cli.filterNotification(msg), Equals, false)
}
}
/*****************************************************************
handleNotification tests
******************************************************************/
var (
positiveNotification = &session.Notification{
Decoded: []map[string]interface{}{
map[string]interface{}{
"daily/mako": []interface{}{float64(103), "tubular"},
},
},
}
negativeNotification = &session.Notification{
Decoded: []map[string]interface{}{
map[string]interface{}{
"daily/mako": []interface{}{float64(102), "tubular"},
},
},
}
)
func (cs *clientSuite) TestHandleNotification(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.systemImageInfo = siInfoRes
endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
cli.notificationsEndp = endp
cli.log = cs.log
c.Check(cli.handleNotification(positiveNotification), IsNil)
// check we sent the notification
args := testibus.GetCallArgs(endp)
c.Assert(args, HasLen, 1)
c.Check(args[0].Member, Equals, "Notify")
c.Check(cs.log.Captured(), Matches, `.* got notification id \d+\s*`)
}
func (cs *clientSuite) TestHandleNotificationNothingToDo(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.systemImageInfo = siInfoRes
endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1))
cli.notificationsEndp = endp
cli.log = cs.log
c.Check(cli.handleNotification(negativeNotification), IsNil)
// check we sent the notification
args := testibus.GetCallArgs(endp)
c.Assert(args, HasLen, 0)
c.Check(cs.log.Captured(), Matches, "")
}
func (cs *clientSuite) TestHandleNotificationFail(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.systemImageInfo = siInfoRes
cli.log = cs.log
endp := testibus.NewTestingEndpoint(nil, condition.Work(false))
cli.notificationsEndp = endp
c.Check(cli.handleNotification(positiveNotification), NotNil)
}
/*****************************************************************
handleClick tests
******************************************************************/
func (cs *clientSuite) TestHandleClick(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
endp := testibus.NewTestingEndpoint(nil, condition.Work(true))
cli.urlDispatcherEndp = endp
// check we don't fail on something random
c.Check(cli.handleClick("something random"), IsNil)
// ... but we don't send anything either
args := testibus.GetCallArgs(endp)
c.Assert(args, HasLen, 0)
// check we worked with the right action id
c.Check(cli.handleClick(ACTION_ID_SNOWFLAKE), IsNil)
// check we sent the notification
args = testibus.GetCallArgs(endp)
c.Assert(args, HasLen, 1)
c.Check(args[0].Member, Equals, "DispatchURL")
c.Check(args[0].Args, DeepEquals, []interface{}{"settings:///system/system-update"})
}
/*****************************************************************
doLoop tests
******************************************************************/
func (cs *clientSuite) TestDoLoopConn(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
cli.systemImageInfo = siInfoRes
cli.connCh = make(chan bool, 1)
cli.connCh <- true
c.Assert(cli.initSession(), IsNil)
ch := make(chan bool, 1)
go cli.doLoop(func(bool) { ch <- true }, func(_ string) error { return nil }, func(_ *session.Notification) error { return nil }, func(error) {})
c.Check(takeNextBool(ch), Equals, true)
}
func (cs *clientSuite) TestDoLoopClick(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
cli.systemImageInfo = siInfoRes
c.Assert(cli.initSession(), IsNil)
aCh := make(chan notifications.RawActionReply, 1)
aCh <- notifications.RawActionReply{}
cli.actionsCh = aCh
ch := make(chan bool, 1)
go cli.doLoop(func(bool) {}, func(_ string) error { ch <- true; return nil }, func(_ *session.Notification) error { return nil }, func(error) {})
c.Check(takeNextBool(ch), Equals, true)
}
func (cs *clientSuite) TestDoLoopNotif(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
cli.systemImageInfo = siInfoRes
c.Assert(cli.initSession(), IsNil)
cli.session.MsgCh = make(chan *session.Notification, 1)
cli.session.MsgCh <- &session.Notification{}
ch := make(chan bool, 1)
go cli.doLoop(func(bool) {}, func(_ string) error { return nil }, func(_ *session.Notification) error { ch <- true; return nil }, func(error) {})
c.Check(takeNextBool(ch), Equals, true)
}
func (cs *clientSuite) TestDoLoopErr(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.log = cs.log
cli.systemImageInfo = siInfoRes
c.Assert(cli.initSession(), IsNil)
cli.session.ErrCh = make(chan error, 1)
cli.session.ErrCh <- nil
ch := make(chan bool, 1)
go cli.doLoop(func(bool) {}, func(_ string) error { return nil }, func(_ *session.Notification) error { return nil }, func(error) { ch <- true })
c.Check(takeNextBool(ch), Equals, true)
}
/*****************************************************************
doStart tests
******************************************************************/
func (cs *clientSuite) TestDoStartWorks(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
one_called := false
two_called := false
one := func() error { one_called = true; return nil }
two := func() error { two_called = true; return nil }
c.Check(cli.doStart(one, two), IsNil)
c.Check(one_called, Equals, true)
c.Check(two_called, Equals, true)
}
func (cs *clientSuite) TestDoStartFailsAsExpected(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
one_called := false
two_called := false
failure := errors.New("Failure")
one := func() error { one_called = true; return failure }
two := func() error { two_called = true; return nil }
c.Check(cli.doStart(one, two), Equals, failure)
c.Check(one_called, Equals, true)
c.Check(two_called, Equals, false)
}
/*****************************************************************
Loop() tests
******************************************************************/
func (cs *clientSuite) TestLoop(c *C) {
cli := NewPushClient(cs.configPath, cs.leveldbPath)
cli.connCh = make(chan bool)
cli.sessionConnectedCh = make(chan uint32)
aCh := make(chan notifications.RawActionReply, 1)
cli.actionsCh = aCh
cli.log = cs.log
cli.notificationsEndp = testibus.NewMultiValuedTestingEndpoint(condition.Work(true),
condition.Work(true), []interface{}{uint32(1), "hello"})
cli.urlDispatcherEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false))
cli.connectivityEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true),
uint32(networkmanager.ConnectedGlobal))
cli.systemImageInfo = siInfoRes
c.Assert(cli.initSession(), IsNil)
cli.session.MsgCh = make(chan *session.Notification)
cli.session.ErrCh = make(chan error)
// we use tick() to make sure things have been through the
// event loop at least once before looking at things;
// otherwise there's a race between what we're trying to look
// at and the loop itself.
tick := func() { cli.sessionConnectedCh <- 42 }
go cli.Loop()
// sessionConnectedCh to nothing in particular, but it'll help sync this test
cli.sessionConnectedCh <- 42
tick()
c.Check(cs.log.Captured(), Matches, "(?ms).*Session connected after 42 attempts$")
// * actionsCh to the click handler/url dispatcher
aCh <- notifications.RawActionReply{ActionId: ACTION_ID_SNOWFLAKE}
tick()
uargs := testibus.GetCallArgs(cli.urlDispatcherEndp)
c.Assert(uargs, HasLen, 1)
c.Check(uargs[0].Member, Equals, "DispatchURL")
// loop() should have connected:
// * connCh to the connectivity checker
c.Check(cli.hasConnectivity, Equals, false)
cli.connCh <- true
tick()
c.Check(cli.hasConnectivity, Equals, true)
cli.connCh <- false
tick()
c.Check(cli.hasConnectivity, Equals, false)
// * session.MsgCh to the notifications handler
cli.session.MsgCh <- positiveNotification
tick()
nargs := testibus.GetCallArgs(cli.notificationsEndp)
c.Check(nargs, HasLen, 1)
// * session.ErrCh to the error handler
cli.session.ErrCh <- nil
tick()
c.Check(cs.log.Captured(), Matches, "(?ms).*session exited.*")
}
/*****************************************************************
Start() tests
******************************************************************/
// XXX this is a hack.
func (cs *clientSuite) hasDbus() bool {
for _, b := range []bus.Bus{bus.SystemBus, bus.SessionBus} {
if b.Endpoint(bus.BusDaemonAddress, cs.log).Dial() != nil {
return false
}
}
return true
}
func (cs *clientSuite) TestStart(c *C) {
if !cs.hasDbus() {
c.Skip("no dbus")
}
cli := NewPushClient(cs.configPath, cs.leveldbPath)
// before start, everything sucks:
// no config,
c.Check(string(cli.config.Addr), Equals, "")
// no device id,
c.Check(cli.deviceId, HasLen, 0)
// no session,
c.Check(cli.session, IsNil)
// no bus,
c.Check(cli.notificationsEndp, IsNil)
// no nuthin'.
// so we start,
err := cli.Start()
// and it works
c.Check(err, IsNil)
// and now everthing is better! We have a config,
c.Check(string(cli.config.Addr), Equals, ":0")
// and a device id,
c.Check(cli.deviceId, HasLen, 128)
// and a session,
c.Check(cli.session, NotNil)
// and a bus,
c.Check(cli.notificationsEndp, NotNil)
// and everthying us just peachy!
}
func (cs *clientSuite) TestStartCanFail(c *C) {
cli := NewPushClient("/does/not/exist", cs.leveldbPath)
// easiest way for it to fail is to feed it a bad config
err := cli.Start()
// and it works. Err. Doesn't.
c.Check(err, NotNil)
}
ubuntu-push-0.2+14.04.20140411/client/gethosts/ 0000755 0000153 0177776 00000000000 12322032700 021351 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/client/gethosts/gethost.go 0000644 0000153 0177776 00000004621 12322032412 023360 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package gethosts implements finding hosts to connect to for delivery of notifications.
package gethosts
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"launchpad.net/ubuntu-push/external/murmur3"
http13 "launchpad.net/ubuntu-push/http13client"
)
// GetHost implements finding hosts to connect to consulting a remote endpoint providing a hash of the device identifier.
type GetHost struct {
hash string
endpointUrl string
cli *http13.Client
}
// New makes a GetHost.
func New(deviceId, endpointUrl string, timeout time.Duration) *GetHost {
hash := murmur3.Sum64([]byte(deviceId))
return &GetHost{
hash: fmt.Sprintf("%x", hash),
endpointUrl: endpointUrl,
cli: &http13.Client{
Transport: &http13.Transport{TLSHandshakeTimeout: timeout},
Timeout: timeout,
},
}
}
type expected struct {
Hosts []string
}
var (
ErrRequest = errors.New("request was not accepted")
ErrInternal = errors.New("remote had an internal error")
ErrTemporary = errors.New("remote had a temporary error")
)
// Get gets a list of hosts consulting the endpoint.
func (gh *GetHost) Get() ([]string, error) {
resp, err := gh.cli.Get(gh.endpointUrl + "?h=" + gh.hash)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
switch {
case resp.StatusCode == http.StatusInternalServerError:
return nil, ErrInternal
case resp.StatusCode > http.StatusInternalServerError:
return nil, ErrTemporary
default:
return nil, ErrRequest
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var parsed expected
err = json.Unmarshal(body, &parsed)
if err != nil {
return nil, ErrTemporary
}
if len(parsed.Hosts) == 0 {
return nil, ErrTemporary
}
return parsed.Hosts, nil
}
ubuntu-push-0.2+14.04.20140411/client/gethosts/gethost_test.go 0000644 0000153 0177776 00000005361 12322032412 024421 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package gethosts
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/external/murmur3"
)
func TestGetHosts(t *testing.T) { TestingT(t) }
type getHostsSuite struct{}
var _ = Suite(&getHostsSuite{})
func (s *getHostsSuite) TestNew(c *C) {
gh := New("foobar", "http://where/hosts", 10*time.Second)
c.Check(gh.hash, Equals, fmt.Sprintf("%x", murmur3.Sum64([]byte("foobar"))))
c.Check(gh.endpointUrl, Equals, "http://where/hosts")
}
func (s *getHostsSuite) TestGet(c *C) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
x := r.FormValue("h")
b, err := json.Marshal(map[string]interface{}{
"hosts": []string{"http://" + x},
})
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(b)
}))
defer ts.Close()
gh := New("foobar", ts.URL, 1*time.Second)
res, err := gh.Get()
c.Assert(err, IsNil)
c.Check(res, DeepEquals, []string{"http://c1130408a700afe0"})
}
func (s *getHostsSuite) TestGetTimeout(c *C) {
started := make(chan bool, 1)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
started <- true
time.Sleep(700 * time.Millisecond)
}))
defer func() {
<-started
ts.Close()
}()
gh := New("foobar", ts.URL, 500*time.Millisecond)
_, err := gh.Get()
c.Check(err, ErrorMatches, ".*closed.*")
}
func (s *getHostsSuite) TestGetErrorScenarios(c *C) {
status := make(chan int, 1)
body := make(chan string, 1)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(<-status)
fmt.Fprintf(w, "%s", <-body)
}))
defer ts.Close()
gh := New("foobar", ts.URL, 1*time.Second)
scenario := func(st int, bdy string, expectedErr error) {
status <- st
body <- bdy
_, err := gh.Get()
c.Check(err, Equals, expectedErr)
}
scenario(http.StatusBadRequest, "", ErrRequest)
scenario(http.StatusInternalServerError, "", ErrInternal)
scenario(http.StatusGatewayTimeout, "", ErrTemporary)
scenario(http.StatusOK, "{", ErrTemporary)
scenario(http.StatusOK, "{}", ErrTemporary)
}
ubuntu-push-0.2+14.04.20140411/client/session/ 0000755 0000153 0177776 00000000000 12322032700 021174 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/client/session/session.go 0000644 0000153 0177776 00000032214 12322032421 023210 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package session handles the minutiae of interacting with
// the Ubuntu Push Notifications server.
package session
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"math/rand"
"net"
"strings"
"sync"
"sync/atomic"
"time"
"launchpad.net/ubuntu-push/client/gethosts"
"launchpad.net/ubuntu-push/client/session/levelmap"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/protocol"
"launchpad.net/ubuntu-push/util"
)
var wireVersionBytes = []byte{protocol.ProtocolWireVersion}
type Notification struct {
TopLevel int64
Decoded []map[string]interface{}
}
type serverMsg struct {
Type string `json:"T"`
protocol.BroadcastMsg
protocol.NotificationsMsg
protocol.ConnBrokenMsg
}
// parseServerAddrSpec recognizes whether spec is a HTTP URL to get
// hosts from or a |-separated list of host:port pairs.
func parseServerAddrSpec(spec string) (hostsEndpoint string, fallbackHosts []string) {
if strings.HasPrefix(spec, "http") {
return spec, nil
}
return "", strings.Split(spec, "|")
}
// ClientSessionState is a way to broadly track the progress of the session
type ClientSessionState uint32
const (
Error ClientSessionState = iota
Disconnected
Connected
Started
Running
)
type hostGetter interface {
Get() ([]string, error)
}
// ClientSessionConfig groups the client session configuration.
type ClientSessionConfig struct {
ConnectTimeout time.Duration
ExchangeTimeout time.Duration
HostsCachingExpiryTime time.Duration
ExpectAllRepairedTime time.Duration
PEM []byte
Info map[string]interface{}
}
// ClientSession holds a client<->server session and its configuration.
type ClientSession struct {
// configuration
DeviceId string
ClientSessionConfig
Levels levelmap.LevelMap
Protocolator func(net.Conn) protocol.Protocol
// hosts
getHost hostGetter
fallbackHosts []string
deliveryHostsTimestamp time.Time
deliveryHosts []string
lastAttemptTimestamp time.Time
leftToTry int
tryHost int
// hook for testing
timeSince func(time.Time) time.Duration
// connection
connLock sync.RWMutex
Connection net.Conn
Log logger.Logger
TLS *tls.Config
proto protocol.Protocol
pingInterval time.Duration
retrier util.AutoRedialer
// status
stateP *uint32
ErrCh chan error
MsgCh chan *Notification
}
func NewSession(serverAddrSpec string, conf ClientSessionConfig,
deviceId string, levelmapFactory func() (levelmap.LevelMap, error),
log logger.Logger) (*ClientSession, error) {
state := uint32(Disconnected)
levels, err := levelmapFactory()
if err != nil {
return nil, err
}
var getHost hostGetter
log.Infof("using addr: %v", serverAddrSpec)
hostsEndpoint, fallbackHosts := parseServerAddrSpec(serverAddrSpec)
if hostsEndpoint != "" {
getHost = gethosts.New(deviceId, hostsEndpoint, conf.ExchangeTimeout)
}
sess := &ClientSession{
ClientSessionConfig: conf,
getHost: getHost,
fallbackHosts: fallbackHosts,
DeviceId: deviceId,
Log: log,
Protocolator: protocol.NewProtocol0,
Levels: levels,
TLS: &tls.Config{InsecureSkipVerify: true}, // XXX
stateP: &state,
timeSince: time.Since,
}
if sess.PEM != nil {
cp := x509.NewCertPool()
ok := cp.AppendCertsFromPEM(sess.PEM)
if !ok {
return nil, errors.New("could not parse certificate")
}
sess.TLS.RootCAs = cp
}
return sess, nil
}
func (sess *ClientSession) State() ClientSessionState {
return ClientSessionState(atomic.LoadUint32(sess.stateP))
}
func (sess *ClientSession) setState(state ClientSessionState) {
atomic.StoreUint32(sess.stateP, uint32(state))
}
func (sess *ClientSession) setConnection(conn net.Conn) {
sess.connLock.Lock()
defer sess.connLock.Unlock()
sess.Connection = conn
}
func (sess *ClientSession) getConnection() net.Conn {
sess.connLock.RLock()
defer sess.connLock.RUnlock()
return sess.Connection
}
// getHosts sets deliveryHosts possibly querying a remote endpoint
func (sess *ClientSession) getHosts() error {
if sess.getHost != nil {
if sess.deliveryHosts != nil && sess.timeSince(sess.deliveryHostsTimestamp) < sess.HostsCachingExpiryTime {
return nil
}
hosts, err := sess.getHost.Get()
if err != nil {
sess.Log.Errorf("getHosts: %v", err)
sess.setState(Error)
return err
}
sess.deliveryHostsTimestamp = time.Now()
sess.deliveryHosts = hosts
} else {
sess.deliveryHosts = sess.fallbackHosts
}
return nil
}
func (sess *ClientSession) resetHosts() {
sess.deliveryHosts = nil
}
// startConnectionAttempt/nextHostToTry help connect iterating over candidate hosts
func (sess *ClientSession) startConnectionAttempt() {
if sess.timeSince(sess.lastAttemptTimestamp) > sess.ExpectAllRepairedTime {
sess.tryHost = 0
}
sess.leftToTry = len(sess.deliveryHosts)
if sess.leftToTry == 0 {
panic("should have got hosts from config or remote at this point")
}
sess.lastAttemptTimestamp = time.Now()
}
func (sess *ClientSession) nextHostToTry() string {
if sess.leftToTry == 0 {
return ""
}
res := sess.deliveryHosts[sess.tryHost]
sess.tryHost = (sess.tryHost + 1) % len(sess.deliveryHosts)
sess.leftToTry--
return res
}
// we reached the Started state, we can retry with the same host if we
// have to retry again
func (sess *ClientSession) started() {
sess.tryHost--
if sess.tryHost == -1 {
sess.tryHost = len(sess.deliveryHosts) - 1
}
sess.setState(Started)
}
// connect to a server using the configuration in the ClientSession
// and set up the connection.
func (sess *ClientSession) connect() error {
sess.startConnectionAttempt()
var err error
var conn net.Conn
for {
host := sess.nextHostToTry()
if host == "" {
sess.setState(Error)
return fmt.Errorf("connect: %s", err)
}
sess.Log.Debugf("trying to connect to: %v", host)
conn, err = net.DialTimeout("tcp", host, sess.ConnectTimeout)
if err == nil {
break
}
}
sess.setConnection(tls.Client(conn, sess.TLS))
sess.setState(Connected)
return nil
}
func (sess *ClientSession) stopRedial() {
if sess.retrier != nil {
sess.retrier.Stop()
sess.retrier = nil
}
}
func (sess *ClientSession) AutoRedial(doneCh chan uint32) {
sess.stopRedial()
sess.retrier = util.NewAutoRedialer(sess)
go func() { doneCh <- sess.retrier.Redial() }()
}
func (sess *ClientSession) Close() {
sess.stopRedial()
sess.doClose()
}
func (sess *ClientSession) doClose() {
sess.connLock.Lock()
defer sess.connLock.Unlock()
if sess.Connection != nil {
sess.Connection.Close()
// we ignore Close errors, on purpose (the thinking being that
// the connection isn't really usable, and you've got nothing
// you could do to recover at this stage).
sess.Connection = nil
}
sess.setState(Disconnected)
}
// handle "ping" messages
func (sess *ClientSession) handlePing() error {
err := sess.proto.WriteMessage(protocol.PingPongMsg{Type: "pong"})
if err == nil {
sess.Log.Debugf("ping.")
} else {
sess.setState(Error)
sess.Log.Errorf("unable to pong: %s", err)
}
return err
}
func (sess *ClientSession) decodeBroadcast(bcast *serverMsg) *Notification {
decoded := make([]map[string]interface{}, 0)
for _, p := range bcast.Payloads {
var v map[string]interface{}
err := json.Unmarshal(p, &v)
if err != nil {
sess.Log.Debugf("expected map in broadcast: %v", err)
continue
}
decoded = append(decoded, v)
}
return &Notification{
TopLevel: bcast.TopLevel,
Decoded: decoded,
}
}
// handle "broadcast" messages
func (sess *ClientSession) handleBroadcast(bcast *serverMsg) error {
err := sess.Levels.Set(bcast.ChanId, bcast.TopLevel)
if err != nil {
sess.setState(Error)
sess.Log.Errorf("unable to set level: %v", err)
sess.proto.WriteMessage(protocol.AckMsg{"nak"})
return err
}
// the server assumes if we ack the broadcast, we've updated
// our levels. Hence the order.
err = sess.proto.WriteMessage(protocol.AckMsg{"ack"})
if err != nil {
sess.setState(Error)
sess.Log.Errorf("unable to ack broadcast: %s", err)
return err
}
sess.Log.Debugf("broadcast chan:%v app:%v topLevel:%d payloads:%s",
bcast.ChanId, bcast.AppId, bcast.TopLevel, bcast.Payloads)
if bcast.ChanId == protocol.SystemChannelId {
// the system channel id, the only one we care about for now
sess.Log.Debugf("sending it over")
sess.MsgCh <- sess.decodeBroadcast(bcast)
sess.Log.Debugf("sent it over")
} else {
sess.Log.Debugf("what is this weird channel, %#v?", bcast.ChanId)
}
return nil
}
// handle "connbroken" messages
func (sess *ClientSession) handleConnBroken(connBroken *serverMsg) error {
sess.setState(Error)
reason := connBroken.Reason
err := fmt.Errorf("server broke connection: %s", reason)
sess.Log.Errorf("%s", err)
switch reason {
case protocol.BrokenHostMismatch:
sess.resetHosts()
}
return err
}
// loop runs the session with the server, emits a stream of events.
func (sess *ClientSession) loop() error {
var err error
var recv serverMsg
sess.setState(Running)
for {
deadAfter := sess.pingInterval + sess.ExchangeTimeout
sess.proto.SetDeadline(time.Now().Add(deadAfter))
err = sess.proto.ReadMessage(&recv)
if err != nil {
sess.setState(Error)
return err
}
switch recv.Type {
case "ping":
err = sess.handlePing()
case "broadcast":
err = sess.handleBroadcast(&recv)
case "connbroken":
err = sess.handleConnBroken(&recv)
}
if err != nil {
return err
}
}
}
// Call this when you've connected and want to start looping.
func (sess *ClientSession) start() error {
conn := sess.getConnection()
err := conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
if err != nil {
sess.setState(Error)
sess.Log.Errorf("unable to start: set deadline: %s", err)
return err
}
_, err = conn.Write(wireVersionBytes)
// The Writer docs: Write must return a non-nil error if it returns
// n < len(p). So, no need to check number of bytes written, hooray.
if err != nil {
sess.setState(Error)
sess.Log.Errorf("unable to start: write version: %s", err)
return err
}
proto := sess.Protocolator(conn)
proto.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
levels, err := sess.Levels.GetAll()
if err != nil {
sess.setState(Error)
sess.Log.Errorf("unable to start: get levels: %v", err)
return err
}
err = proto.WriteMessage(protocol.ConnectMsg{
Type: "connect",
DeviceId: sess.DeviceId,
// xxx get the SSO Authorization string from the phone
Authorization: "",
Levels: levels,
Info: sess.Info,
})
if err != nil {
sess.setState(Error)
sess.Log.Errorf("unable to start: connect: %s", err)
return err
}
var connAck protocol.ConnAckMsg
err = proto.ReadMessage(&connAck)
if err != nil {
sess.setState(Error)
sess.Log.Errorf("unable to start: connack: %s", err)
return err
}
if connAck.Type != "connack" {
sess.setState(Error)
return fmt.Errorf("expecting CONNACK, got %#v", connAck.Type)
}
pingInterval, err := time.ParseDuration(connAck.Params.PingInterval)
if err != nil {
sess.setState(Error)
sess.Log.Errorf("unable to start: parse ping interval: %s", err)
return err
}
sess.proto = proto
sess.pingInterval = pingInterval
sess.Log.Debugf("Connected %v.", conn.RemoteAddr())
sess.started() // deals with choosing which host to retry with as well
return nil
}
// run calls connect, and if it works it calls start, and if it works
// it runs loop in a goroutine, and ships its return value over ErrCh.
func (sess *ClientSession) run(closer func(), hostGetter, connecter, starter, looper func() error) error {
closer()
err := hostGetter()
if err != nil {
return err
}
err = connecter()
if err == nil {
err = starter()
if err == nil {
sess.ErrCh = make(chan error, 1)
sess.MsgCh = make(chan *Notification)
go func() { sess.ErrCh <- looper() }()
}
}
return err
}
// This Jitter returns a random time.Duration somewhere in [-spread, spread].
func (sess *ClientSession) Jitter(spread time.Duration) time.Duration {
if spread < 0 {
panic("spread must be non-negative")
}
n := int64(spread)
return time.Duration(rand.Int63n(2*n+1) - n)
}
// Dial takes the session from newly created (or newly disconnected)
// to running the main loop.
func (sess *ClientSession) Dial() error {
if sess.Protocolator == nil {
// a missing protocolator means you've willfully overridden
// it; returning an error here would prompt AutoRedial to just
// keep on trying.
panic("can't Dial() without a protocol constructor.")
}
return sess.run(sess.doClose, sess.getHosts, sess.connect, sess.start, sess.loop)
}
func init() {
rand.Seed(time.Now().Unix()) // good enough for us (we're not using it for crypto)
}
ubuntu-push-0.2+14.04.20140411/client/session/levelmap/ 0000755 0000153 0177776 00000000000 12322032700 023001 5 ustar pbuser nogroup 0000000 0000000 ubuntu-push-0.2+14.04.20140411/client/session/levelmap/sqlevelmap_test.go 0000644 0000153 0177776 00000005106 12322032412 026542 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package levelmap
import (
_ "code.google.com/p/gosqlite/sqlite3"
"database/sql"
. "launchpad.net/gocheck"
)
type sqlmSuite struct{ lmSuite }
var _ = Suite(&sqlmSuite{})
func (s *sqlmSuite) SetUpSuite(c *C) {
s.constructor = func() (LevelMap, error) { return NewSqliteLevelMap(":memory:") }
}
func (s *sqlmSuite) TestNewCanFail(c *C) {
m, err := NewSqliteLevelMap("/does/not/exist")
c.Assert(m, IsNil)
c.Check(err, NotNil)
}
func (s *sqlmSuite) TestSetCanFail(c *C) {
dir := c.MkDir()
filename := dir + "test.db"
db, err := sql.Open("sqlite3", filename)
c.Assert(err, IsNil)
// create the wrong kind of table
_, err = db.Exec("CREATE TABLE level_map (foo)")
c.Assert(err, IsNil)
//
m, err := NewSqliteLevelMap(filename)
c.Check(err, IsNil)
c.Assert(m, NotNil)
err = m.Set("foo", 42)
c.Check(err, ErrorMatches, "cannot set .*")
}
func (s *sqlmSuite) TestGetAllCanFail(c *C) {
dir := c.MkDir()
filename := dir + "test.db"
db, err := sql.Open("sqlite3", filename)
c.Assert(err, IsNil)
// create the wrong kind of table
_, err = db.Exec("CREATE TABLE level_map AS SELECT 'what'")
c.Assert(err, IsNil)
//
m, err := NewSqliteLevelMap(filename)
c.Check(err, IsNil)
c.Assert(m, NotNil)
all, err := m.GetAll()
c.Check(all, IsNil)
c.Check(err, ErrorMatches, "cannot read level .*")
}
func (s *sqlmSuite) TestGetAllCanFailDifferently(c *C) {
dir := c.MkDir()
filename := dir + "test.db"
db, err := sql.Open("sqlite3", filename)
c.Assert(err, IsNil)
// create a view with the name the table will have
_, err = db.Exec("CREATE TABLE foo (foo)")
c.Assert(err, IsNil)
_, err = db.Exec("CREATE VIEW level_map AS SELECT * FROM foo")
c.Assert(err, IsNil)
// break the view
_, err = db.Exec("DROP TABLE foo")
c.Assert(err, IsNil)
//
m, err := NewSqliteLevelMap(filename)
c.Check(err, IsNil)
c.Assert(m, NotNil)
all, err := m.GetAll()
c.Check(all, IsNil)
c.Check(err, ErrorMatches, "cannot retrieve levels .*")
}
ubuntu-push-0.2+14.04.20140411/client/session/levelmap/levelmap.go 0000644 0000153 0177776 00000002617 12322032412 025143 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
// Package levelmap holds implementations of the LevelMap that the client
// session uses to keep track of what messages it has seen.
package levelmap
type LevelMap interface {
// Set() (re)sets the given level to the given value.
Set(level string, top int64) error
// GetAll() returns a "simple" map of the current levels.
GetAll() (map[string]int64, error)
}
type mapLevelMap map[string]int64
func (m *mapLevelMap) Set(level string, top int64) error {
(*m)[level] = top
return nil
}
func (m *mapLevelMap) GetAll() (map[string]int64, error) {
return map[string]int64(*m), nil
}
var _ LevelMap = &mapLevelMap{}
// NewLevelMap returns an implementation of LevelMap that is memory-based and
// does not save state.
func NewLevelMap() (LevelMap, error) {
return &mapLevelMap{}, nil
}
ubuntu-push-0.2+14.04.20140411/client/session/levelmap/sqlevelmap.go 0000644 0000153 0177776 00000003762 12322032412 025511 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package levelmap
import (
_ "code.google.com/p/gosqlite/sqlite3"
"database/sql"
"fmt"
)
type sqliteLevelMap struct {
db *sql.DB
}
// NewSqliteLevelMap returns an implementation of LevelMap that
// persists the map in an sqlite database.
func NewSqliteLevelMap(filename string) (LevelMap, error) {
db, err := sql.Open("sqlite3", filename)
if err != nil {
return nil, fmt.Errorf("cannot open sqlite level map %#v: %v", filename, err)
}
_, err = db.Exec("CREATE TABLE IF NOT EXISTS level_map (level text primary key, top integer)")
if err != nil {
return nil, fmt.Errorf("cannot (re)create sqlite level map table: %v", err)
}
return &sqliteLevelMap{db}, nil
}
func (pm *sqliteLevelMap) Set(level string, top int64) error {
_, err := pm.db.Exec("REPLACE INTO level_map (level, top) VALUES (?, ?)", level, top)
if err != nil {
return fmt.Errorf("cannot set %#v to %#v in level map: %v", level, top, err)
}
return nil
}
func (pm *sqliteLevelMap) GetAll() (map[string]int64, error) {
rows, err := pm.db.Query("SELECT * FROM level_map")
if err != nil {
return nil, fmt.Errorf("cannot retrieve levels from sqlite level map: %v", err)
}
m := map[string]int64{}
for rows.Next() {
var level string
var top int64
err = rows.Scan(&level, &top)
if err != nil {
return nil, fmt.Errorf("cannot read level from sqlite level map: %v", err)
}
m[level] = top
}
return m, nil
}
ubuntu-push-0.2+14.04.20140411/client/session/levelmap/levelmap_test.go 0000644 0000153 0177776 00000002763 12322032412 026204 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package levelmap
import (
. "launchpad.net/gocheck"
"testing"
)
func TestLevelMap(t *testing.T) { TestingT(t) }
type lmSuite struct {
constructor func() (LevelMap, error)
}
var _ = Suite(&lmSuite{})
func (s *lmSuite) SetUpSuite(c *C) {
s.constructor = NewLevelMap
}
func (s *lmSuite) TestAllTheThings(c *C) {
var err error
var lm LevelMap
// checks NewLevelMap returns a LevelMap
lm, err = s.constructor()
// and that it works
c.Assert(err, IsNil)
// setting a couple of things, sets them
c.Check(lm.Set("this", 12), IsNil)
c.Check(lm.Set("that", 42), IsNil)
all, err := lm.GetAll()
c.Check(err, IsNil)
c.Check(all, DeepEquals, map[string]int64{"this": 12, "that": 42})
// re-setting one of them, resets it
c.Check(lm.Set("this", 999), IsNil)
all, err = lm.GetAll()
c.Check(err, IsNil)
c.Check(all, DeepEquals, map[string]int64{"this": 999, "that": 42})
// huzzah
}
ubuntu-push-0.2+14.04.20140411/client/session/session_test.go 0000644 0000153 0177776 00000110625 12322032421 024252 0 ustar pbuser nogroup 0000000 0000000 /*
Copyright 2013-2014 Canonical Ltd.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3, as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranties of
MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see .
*/
package session
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/client/session/levelmap"
//"launchpad.net/ubuntu-push/client/gethosts"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/protocol"
helpers "launchpad.net/ubuntu-push/testing"
"launchpad.net/ubuntu-push/testing/condition"
)
func TestSession(t *testing.T) { TestingT(t) }
//
// helpers! candidates to live in their own ../testing/ package.
//
type xAddr string
func (x xAddr) Network() string { return "<:>" }
func (x xAddr) String() string { return string(x) }
// testConn (roughly based on the one in protocol_test)
type testConn struct {
Name string
Deadlines []time.Duration
Writes [][]byte
WriteCondition condition.Interface
DeadlineCondition condition.Interface
CloseCondition condition.Interface
}
func (tc *testConn) LocalAddr() net.Addr { return xAddr(tc.Name) }
func (tc *testConn) RemoteAddr() net.Addr { return xAddr(tc.Name) }
func (tc *testConn) Close() error {
if tc.CloseCondition == nil || tc.CloseCondition.OK() {
return nil
} else {
return errors.New("closer on fire")
}
}
func (tc *testConn) SetDeadline(t time.Time) error {
tc.Deadlines = append(tc.Deadlines, t.Sub(time.Now()))
if tc.DeadlineCondition == nil || tc.DeadlineCondition.OK() {
return nil
} else {
return errors.New("deadliner on fire")
}
}
func (tc *testConn) SetReadDeadline(t time.Time) error { panic("SetReadDeadline not implemented.") }
func (tc *testConn) SetWriteDeadline(t time.Time) error { panic("SetWriteDeadline not implemented.") }
func (tc *testConn) Read(buf []byte) (n int, err error) { panic("Read not implemented.") }
func (tc *testConn) Write(buf []byte) (int, error) {
store := make([]byte, len(buf))
copy(store, buf)
tc.Writes = append(tc.Writes, store)
if tc.WriteCondition == nil || tc.WriteCondition.OK() {
return len(store), nil
} else {
return -1, errors.New("writer on fire")
}
}
// test protocol (from session_test)
type testProtocol struct {
up chan interface{}
down chan interface{}
}
// takeNext takes a value from given channel with a 5s timeout
func takeNext(ch <-chan interface{}) interface{} {
select {
case <-time.After(5 * time.Second):
panic("test protocol exchange stuck: too long waiting")
case v := <-ch:
return v
}
return nil
}
func (c *testProtocol) SetDeadline(t time.Time) {
deadAfter := t.Sub(time.Now())
deadAfter = (deadAfter + time.Millisecond/2) / time.Millisecond * time.Millisecond
c.down <- fmt.Sprintf("deadline %v", deadAfter)
}
func (c *testProtocol) ReadMessage(dest interface{}) error {
switch v := takeNext(c.up).(type) {
case error:
return v
default:
// make sure JSON.Unmarshal works with dest
var marshalledMsg []byte
marshalledMsg, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("can't jsonify test value %v: %s", v, err)
}
return json.Unmarshal(marshalledMsg, dest)
}
return nil
}
func (c *testProtocol) WriteMessage(src interface{}) error {
// make sure JSON.Marshal works with src
_, err := json.Marshal(src)
if err != nil {
return err
}
val := reflect.ValueOf(src)
if val.Kind() == reflect.Ptr {
src = val.Elem().Interface()
}
c.down <- src
switch v := takeNext(c.up).(type) {
case error:
return v
}
return nil
}
// brokenLevelMap is a LevelMap that always breaks
type brokenLevelMap struct{}
func (*brokenLevelMap) Set(string, int64) error { return errors.New("broken.") }
func (*brokenLevelMap) GetAll() (map[string]int64, error) { return nil, errors.New("broken.") }
/////
type clientSessionSuite struct {
log logger.Logger
lvls func() (levelmap.LevelMap, error)
}
func (cs *clientSessionSuite) SetUpTest(c *C) {
cs.log = helpers.NewTestLogger(c, "debug")
}
// in-memory level map testing
var _ = Suite(&clientSessionSuite{lvls: levelmap.NewLevelMap})
// sqlite level map testing
type clientSqlevelsSessionSuite struct{ clientSessionSuite }
var _ = Suite(&clientSqlevelsSessionSuite{})
func (cs *clientSqlevelsSessionSuite) SetUpSuite(c *C) {
cs.lvls = func() (levelmap.LevelMap, error) { return levelmap.NewSqliteLevelMap(":memory:") }
}
/****************************************************************
parseServerAddrSpec() tests
****************************************************************/
func (cs *clientSessionSuite) TestParseServerAddrSpec(c *C) {
hEp, fallbackHosts := parseServerAddrSpec("http://foo/hosts")
c.Check(hEp, Equals, "http://foo/hosts")
c.Check(fallbackHosts, IsNil)
hEp, fallbackHosts = parseServerAddrSpec("foo:443")
c.Check(hEp, Equals, "")
c.Check(fallbackHosts, DeepEquals, []string{"foo:443"})
hEp, fallbackHosts = parseServerAddrSpec("foo:443|bar:443")
c.Check(hEp, Equals, "")
c.Check(fallbackHosts, DeepEquals, []string{"foo:443", "bar:443"})
}
/****************************************************************
NewSession() tests
****************************************************************/
var dummyConf = ClientSessionConfig{}
func (cs *clientSessionSuite) TestNewSessionPlainWorks(c *C) {
sess, err := NewSession("foo:443", dummyConf, "", cs.lvls, cs.log)
c.Check(sess, NotNil)
c.Check(err, IsNil)
c.Check(sess.fallbackHosts, DeepEquals, []string{"foo:443"})
// but no root CAs set
c.Check(sess.TLS.RootCAs, IsNil)
c.Check(sess.State(), Equals, Disconnected)
}
func (cs *clientSessionSuite) TestNewSessionHostEndpointWorks(c *C) {
sess, err := NewSession("http://foo/hosts", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
c.Check(sess.getHost, NotNil)
}
var certfile string = helpers.SourceRelative("../../server/acceptance/ssl/testing.cert")
var pem, _ = ioutil.ReadFile(certfile)
func (cs *clientSessionSuite) TestNewSessionPEMWorks(c *C) {
conf := ClientSessionConfig{PEM: pem}
sess, err := NewSession("", conf, "wah", cs.lvls, cs.log)
c.Check(sess, NotNil)
c.Assert(err, IsNil)
c.Check(sess.TLS.RootCAs, NotNil)
}
func (cs *clientSessionSuite) TestNewSessionBadPEMFileContentFails(c *C) {
badpem := []byte("This is not the PEM you're looking for.")
conf := ClientSessionConfig{PEM: badpem}
sess, err := NewSession("", conf, "wah", cs.lvls, cs.log)
c.Check(sess, IsNil)
c.Check(err, NotNil)
}
func (cs *clientSessionSuite) TestNewSessionBadLevelMapFails(c *C) {
ferr := func() (levelmap.LevelMap, error) { return nil, errors.New("Busted.") }
sess, err := NewSession("", dummyConf, "wah", ferr, cs.log)
c.Check(sess, IsNil)
c.Assert(err, NotNil)
}
/****************************************************************
getHosts() tests
****************************************************************/
func (cs *clientSessionSuite) TestGetHostsFallback(c *C) {
fallback := []string{"foo:443", "bar:443"}
sess := &ClientSession{fallbackHosts: fallback}
err := sess.getHosts()
c.Assert(err, IsNil)
c.Check(sess.deliveryHosts, DeepEquals, fallback)
}
type testHostGetter struct {
hosts []string
err error
}
func (thg *testHostGetter) Get() ([]string, error) {
return thg.hosts, thg.err
}
func (cs *clientSessionSuite) TestGetHostsRemote(c *C) {
hostGetter := &testHostGetter{[]string{"foo:443", "bar:443"}, nil}
sess := &ClientSession{getHost: hostGetter, timeSince: time.Since}
err := sess.getHosts()
c.Assert(err, IsNil)
c.Check(sess.deliveryHosts, DeepEquals, []string{"foo:443", "bar:443"})
}
func (cs *clientSessionSuite) TestGetHostsRemoteError(c *C) {
sess, err := NewSession("", dummyConf, "", cs.lvls, cs.log)
c.Assert(err, IsNil)
hostsErr := errors.New("failed")
hostGetter := &testHostGetter{nil, hostsErr}
sess.getHost = hostGetter
err = sess.getHosts()
c.Assert(err, Equals, hostsErr)
c.Check(sess.deliveryHosts, IsNil)
c.Check(sess.State(), Equals, Error)
}
func (cs *clientSessionSuite) TestGetHostsRemoteCaching(c *C) {
hostGetter := &testHostGetter{[]string{"foo:443", "bar:443"}, nil}
sess := &ClientSession{
getHost: hostGetter,
ClientSessionConfig: ClientSessionConfig{
HostsCachingExpiryTime: 2 * time.Hour,
},
timeSince: time.Since,
}
err := sess.getHosts()
c.Assert(err, IsNil)
hostGetter.hosts = []string{"baz:443"}
// cached
err = sess.getHosts()
c.Assert(err, IsNil)
c.Check(sess.deliveryHosts, DeepEquals, []string{"foo:443", "bar:443"})
// expired
sess.timeSince = func(ts time.Time) time.Duration {
return 3 * time.Hour
}
err = sess.getHosts()
c.Assert(err, IsNil)
c.Check(sess.deliveryHosts, DeepEquals, []string{"baz:443"})
}
func (cs *clientSessionSuite) TestGetHostsRemoteCachingReset(c *C) {
hostGetter := &testHostGetter{[]string{"foo:443", "bar:443"}, nil}
sess := &ClientSession{
getHost: hostGetter,
ClientSessionConfig: ClientSessionConfig{
HostsCachingExpiryTime: 2 * time.Hour,
},
timeSince: time.Since,
}
err := sess.getHosts()
c.Assert(err, IsNil)
hostGetter.hosts = []string{"baz:443"}
// cached
err = sess.getHosts()
c.Assert(err, IsNil)
c.Check(sess.deliveryHosts, DeepEquals, []string{"foo:443", "bar:443"})
// reset
sess.resetHosts()
err = sess.getHosts()
c.Assert(err, IsNil)
c.Check(sess.deliveryHosts, DeepEquals, []string{"baz:443"})
}
/****************************************************************
startConnectionAttempt()/nextHostToTry()/started tests
****************************************************************/
func (cs *clientSessionSuite) TestStartConnectionAttempt(c *C) {
since := time.Since(time.Time{})
sess := &ClientSession{
ClientSessionConfig: ClientSessionConfig{
ExpectAllRepairedTime: 10 * time.Second,
},
timeSince: func(ts time.Time) time.Duration {
return since
},
deliveryHosts: []string{"foo:443", "bar:443"},
}
// start from first host
sess.startConnectionAttempt()
c.Check(sess.lastAttemptTimestamp, Not(Equals), 0)
c.Check(sess.tryHost, Equals, 0)
c.Check(sess.leftToTry, Equals, 2)
since = 1 * time.Second
sess.tryHost = 1
// just continue
sess.startConnectionAttempt()
c.Check(sess.tryHost, Equals, 1)
sess.tryHost = 2
}
func (cs *clientSessionSuite) TestStartConnectionAttemptNoHostsPanic(c *C) {
since := time.Since(time.Time{})
sess := &ClientSession{
ClientSessionConfig: ClientSessionConfig{
ExpectAllRepairedTime: 10 * time.Second,
},
timeSince: func(ts time.Time) time.Duration {
return since
},
}
c.Check(sess.startConnectionAttempt, PanicMatches, "should have got hosts from config or remote at this point")
}
func (cs *clientSessionSuite) TestNextHostToTry(c *C) {
sess := &ClientSession{
deliveryHosts: []string{"foo:443", "bar:443", "baz:443"},
tryHost: 0,
leftToTry: 3,
}
c.Check(sess.nextHostToTry(), Equals, "foo:443")
c.Check(sess.nextHostToTry(), Equals, "bar:443")
c.Check(sess.nextHostToTry(), Equals, "baz:443")
c.Check(sess.nextHostToTry(), Equals, "")
c.Check(sess.nextHostToTry(), Equals, "")
c.Check(sess.tryHost, Equals, 0)
sess.leftToTry = 3
sess.tryHost = 1
c.Check(sess.nextHostToTry(), Equals, "bar:443")
c.Check(sess.nextHostToTry(), Equals, "baz:443")
c.Check(sess.nextHostToTry(), Equals, "foo:443")
c.Check(sess.nextHostToTry(), Equals, "")
c.Check(sess.nextHostToTry(), Equals, "")
c.Check(sess.tryHost, Equals, 1)
}
func (cs *clientSessionSuite) TestStarted(c *C) {
sess, err := NewSession("", dummyConf, "", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.deliveryHosts = []string{"foo:443", "bar:443", "baz:443"}
sess.tryHost = 1
sess.started()
c.Check(sess.tryHost, Equals, 0)
c.Check(sess.State(), Equals, Started)
sess.started()
c.Check(sess.tryHost, Equals, 2)
}
/****************************************************************
connect() tests
****************************************************************/
func (cs *clientSessionSuite) TestConnectFailsWithNoAddress(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.deliveryHosts = []string{"nowhere"}
err = sess.connect()
c.Check(err, ErrorMatches, ".*connect.*address.*")
c.Check(sess.State(), Equals, Error)
}
func (cs *clientSessionSuite) TestConnectConnects(c *C) {
srv, err := net.Listen("tcp", "localhost:0")
c.Assert(err, IsNil)
defer srv.Close()
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.deliveryHosts = []string{srv.Addr().String()}
err = sess.connect()
c.Check(err, IsNil)
c.Check(sess.Connection, NotNil)
c.Check(sess.State(), Equals, Connected)
}
func (cs *clientSessionSuite) TestConnectSecondConnects(c *C) {
srv, err := net.Listen("tcp", "localhost:0")
c.Assert(err, IsNil)
defer srv.Close()
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.deliveryHosts = []string{"nowhere", srv.Addr().String()}
err = sess.connect()
c.Check(err, IsNil)
c.Check(sess.Connection, NotNil)
c.Check(sess.State(), Equals, Connected)
c.Check(sess.tryHost, Equals, 0)
}
func (cs *clientSessionSuite) TestConnectConnectFail(c *C) {
srv, err := net.Listen("tcp", "localhost:0")
c.Assert(err, IsNil)
sess, err := NewSession(srv.Addr().String(), dummyConf, "wah", cs.lvls, cs.log)
srv.Close()
c.Assert(err, IsNil)
sess.deliveryHosts = []string{srv.Addr().String()}
err = sess.connect()
c.Check(err, ErrorMatches, ".*connection refused")
c.Check(sess.State(), Equals, Error)
}
/****************************************************************
Close() tests
****************************************************************/
func (cs *clientSessionSuite) TestClose(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Connection = &testConn{Name: "TestClose"}
sess.Close()
c.Check(sess.Connection, IsNil)
c.Check(sess.State(), Equals, Disconnected)
}
func (cs *clientSessionSuite) TestCloseTwice(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Connection = &testConn{Name: "TestCloseTwice"}
sess.Close()
c.Check(sess.Connection, IsNil)
sess.Close()
c.Check(sess.Connection, IsNil)
c.Check(sess.State(), Equals, Disconnected)
}
func (cs *clientSessionSuite) TestCloseFails(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Connection = &testConn{Name: "TestCloseFails", CloseCondition: condition.Work(false)}
sess.Close()
c.Check(sess.Connection, IsNil) // nothing you can do to clean up anyway
c.Check(sess.State(), Equals, Disconnected)
}
type derp struct{ stopped bool }
func (*derp) Redial() uint32 { return 0 }
func (d *derp) Stop() { d.stopped = true }
func (cs *clientSessionSuite) TestCloseStopsRetrier(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
ar := new(derp)
sess.retrier = ar
c.Check(ar.stopped, Equals, false)
sess.Close()
c.Check(ar.stopped, Equals, true)
sess.Close() // double close check
c.Check(ar.stopped, Equals, true)
}
/****************************************************************
AutoRedial() tests
****************************************************************/
func (cs *clientSessionSuite) TestAutoRedialWorks(c *C) {
// checks that AutoRedial sets up a retrier and tries redialing it
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
ar := new(derp)
sess.retrier = ar
c.Check(ar.stopped, Equals, false)
sess.AutoRedial(nil)
c.Check(ar.stopped, Equals, true)
}
func (cs *clientSessionSuite) TestAutoRedialStopsRetrier(c *C) {
// checks that AutoRedial stops the previous retrier
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
ch := make(chan uint32)
c.Check(sess.retrier, IsNil)
sess.AutoRedial(ch)
c.Assert(sess.retrier, NotNil)
sess.retrier.Stop()
c.Check(<-ch, Not(Equals), 0)
}
/****************************************************************
handlePing() tests
****************************************************************/
type msgSuite struct {
sess *ClientSession
upCh chan interface{}
downCh chan interface{}
errCh chan error
}
var _ = Suite(&msgSuite{})
func (s *msgSuite) SetUpTest(c *C) {
var err error
conf := ClientSessionConfig{
ExchangeTimeout: time.Millisecond,
}
s.sess, err = NewSession("", conf, "wah", levelmap.NewLevelMap, helpers.NewTestLogger(c, "debug"))
c.Assert(err, IsNil)
s.sess.Connection = &testConn{Name: "TestHandle*"}
s.errCh = make(chan error, 1)
s.upCh = make(chan interface{}, 5)
s.downCh = make(chan interface{}, 5)
s.sess.proto = &testProtocol{up: s.upCh, down: s.downCh}
// make the message channel buffered
s.sess.MsgCh = make(chan *Notification, 5)
}
func (s *msgSuite) TestHandlePingWorks(c *C) {
s.upCh <- nil // no error
c.Check(s.sess.handlePing(), IsNil)
c.Assert(len(s.downCh), Equals, 1)
c.Check(<-s.downCh, Equals, protocol.PingPongMsg{Type: "pong"})
}
func (s *msgSuite) TestHandlePingHandlesPongWriteError(c *C) {
failure := errors.New("Pong")
s.upCh <- failure
c.Check(s.sess.handlePing(), Equals, failure)
c.Assert(len(s.downCh), Equals, 1)
c.Check(<-s.downCh, Equals, protocol.PingPongMsg{Type: "pong"})
c.Check(s.sess.State(), Equals, Error)
}
/****************************************************************
handleBroadcast() tests
****************************************************************/
func (s *msgSuite) TestHandleBroadcastWorks(c *C) {
msg := serverMsg{"broadcast",
protocol.BroadcastMsg{
Type: "broadcast",
AppId: "--ignored--",
ChanId: "0",
TopLevel: 2,
Payloads: []json.RawMessage{
json.RawMessage(`{"img1/m1":[101,"tubular"]}`),
json.RawMessage("false"), // shouldn't happen but robust
json.RawMessage(`{"img1/m1":[102,"tubular"]}`),
},
}, protocol.NotificationsMsg{}, protocol.ConnBrokenMsg{}}
go func() { s.errCh <- s.sess.handleBroadcast(&msg) }()
c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
s.upCh <- nil // ack ok
c.Check(<-s.errCh, Equals, nil)
c.Assert(len(s.sess.MsgCh), Equals, 1)
c.Check(<-s.sess.MsgCh, DeepEquals, &Notification{
TopLevel: 2,
Decoded: []map[string]interface{}{
map[string]interface{}{
"img1/m1": []interface{}{float64(101), "tubular"},
},
map[string]interface{}{
"img1/m1": []interface{}{float64(102), "tubular"},
},
},
})
// and finally, the session keeps track of the levels
levels, err := s.sess.Levels.GetAll()
c.Check(err, IsNil)
c.Check(levels, DeepEquals, map[string]int64{"0": 2})
}
func (s *msgSuite) TestHandleBroadcastBadAckWrite(c *C) {
msg := serverMsg{"broadcast",
protocol.BroadcastMsg{
Type: "broadcast",
AppId: "APP",
ChanId: "0",
TopLevel: 2,
Payloads: []json.RawMessage{json.RawMessage(`{"b":1}`)},
}, protocol.NotificationsMsg{}, protocol.ConnBrokenMsg{}}
go func() { s.errCh <- s.sess.handleBroadcast(&msg) }()
c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
failure := errors.New("ACK ACK ACK")
s.upCh <- failure
c.Assert(<-s.errCh, Equals, failure)
c.Check(s.sess.State(), Equals, Error)
}
func (s *msgSuite) TestHandleBroadcastWrongChannel(c *C) {
msg := serverMsg{"broadcast",
protocol.BroadcastMsg{
Type: "broadcast",
AppId: "APP",
ChanId: "something awful",
TopLevel: 2,
Payloads: []json.RawMessage{json.RawMessage(`{"b":1}`)},
}, protocol.NotificationsMsg{}, protocol.ConnBrokenMsg{}}
go func() { s.errCh <- s.sess.handleBroadcast(&msg) }()
c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
s.upCh <- nil // ack ok
c.Check(<-s.errCh, IsNil)
c.Check(len(s.sess.MsgCh), Equals, 0)
}
func (s *msgSuite) TestHandleBroadcastWrongBrokenLevelmap(c *C) {
s.sess.Levels = &brokenLevelMap{}
msg := serverMsg{"broadcast",
protocol.BroadcastMsg{
Type: "broadcast",
AppId: "--ignored--",
ChanId: "0",
TopLevel: 2,
Payloads: []json.RawMessage{json.RawMessage(`{"b":1}`)},
}, protocol.NotificationsMsg{}, protocol.ConnBrokenMsg{}}
go func() { s.errCh <- s.sess.handleBroadcast(&msg) }()
s.upCh <- nil // ack ok
// start returns with error
c.Check(<-s.errCh, Not(Equals), nil)
// no message sent out
c.Check(len(s.sess.MsgCh), Equals, 0)
// and nak'ed it
c.Check(len(s.downCh), Equals, 1)
c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"nak"})
}
/****************************************************************
handleConnBroken() tests
****************************************************************/
func (s *msgSuite) TestHandleConnBrokenUnkwown(c *C) {
msg := serverMsg{"connbroken",
protocol.BroadcastMsg{}, protocol.NotificationsMsg{},
protocol.ConnBrokenMsg{
Reason: "REASON",
},
}
go func() { s.errCh <- s.sess.handleConnBroken(&msg) }()
c.Check(<-s.errCh, ErrorMatches, "server broke connection: REASON")
c.Check(s.sess.State(), Equals, Error)
}
func (s *msgSuite) TestHandleConnBrokenHostMismatch(c *C) {
msg := serverMsg{"connbroken",
protocol.BroadcastMsg{}, protocol.NotificationsMsg{},
protocol.ConnBrokenMsg{
Reason: protocol.BrokenHostMismatch,
},
}
s.sess.deliveryHosts = []string{"foo:443", "bar:443"}
go func() { s.errCh <- s.sess.handleConnBroken(&msg) }()
c.Check(<-s.errCh, ErrorMatches, "server broke connection: host-mismatch")
c.Check(s.sess.State(), Equals, Error)
// hosts were reset
c.Check(s.sess.deliveryHosts, IsNil)
}
/****************************************************************
loop() tests
****************************************************************/
type loopSuite msgSuite
var _ = Suite(&loopSuite{})
func (s *loopSuite) SetUpTest(c *C) {
(*msgSuite)(s).SetUpTest(c)
s.sess.Connection.(*testConn).Name = "TestLoop*"
go func() {
s.errCh <- s.sess.loop()
}()
}
func (s *loopSuite) TestLoopReadError(c *C) {
c.Check(s.sess.State(), Equals, Running)
s.upCh <- errors.New("Read")
err := <-s.errCh
c.Check(err, ErrorMatches, "Read")
c.Check(s.sess.State(), Equals, Error)
}
func (s *loopSuite) TestLoopPing(c *C) {
c.Check(s.sess.State(), Equals, Running)
c.Check(takeNext(s.downCh), Equals, "deadline 1ms")
s.upCh <- protocol.PingPongMsg{Type: "ping"}
c.Check(takeNext(s.downCh), Equals, protocol.PingPongMsg{Type: "pong"})
failure := errors.New("pong")
s.upCh <- failure
c.Check(<-s.errCh, Equals, failure)
}
func (s *loopSuite) TestLoopLoopsDaLoop(c *C) {
c.Check(s.sess.State(), Equals, Running)
for i := 1; i < 10; i++ {
c.Check(takeNext(s.downCh), Equals, "deadline 1ms")
s.upCh <- protocol.PingPongMsg{Type: "ping"}
c.Check(takeNext(s.downCh), Equals, protocol.PingPongMsg{Type: "pong"})
s.upCh <- nil
}
failure := errors.New("pong")
s.upCh <- failure
c.Check(<-s.errCh, Equals, failure)
}
func (s *loopSuite) TestLoopBroadcast(c *C) {
c.Check(s.sess.State(), Equals, Running)
b := &protocol.BroadcastMsg{
Type: "broadcast",
AppId: "--ignored--",
ChanId: "0",
TopLevel: 2,
Payloads: []json.RawMessage{json.RawMessage(`{"b":1}`)},
}
c.Check(takeNext(s.downCh), Equals, "deadline 1ms")
s.upCh <- b
c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"})
failure := errors.New("ack")
s.upCh <- failure
c.Check(<-s.errCh, Equals, failure)
}
func (s *loopSuite) TestLoopConnBroken(c *C) {
c.Check(s.sess.State(), Equals, Running)
broken := protocol.ConnBrokenMsg{
Type: "connbroken",
Reason: "REASON",
}
c.Check(takeNext(s.downCh), Equals, "deadline 1ms")
s.upCh <- broken
c.Check(<-s.errCh, NotNil)
}
/****************************************************************
start() tests
****************************************************************/
func (cs *clientSessionSuite) TestStartFailsIfSetDeadlineFails(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Connection = &testConn{Name: "TestStartFailsIfSetDeadlineFails",
DeadlineCondition: condition.Work(false)} // setdeadline will fail
err = sess.start()
c.Check(err, ErrorMatches, ".*deadline.*")
c.Check(sess.State(), Equals, Error)
}
func (cs *clientSessionSuite) TestStartFailsIfWriteFails(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Connection = &testConn{Name: "TestStartFailsIfWriteFails",
WriteCondition: condition.Work(false)} // write will fail
err = sess.start()
c.Check(err, ErrorMatches, ".*write.*")
c.Check(sess.State(), Equals, Error)
}
func (cs *clientSessionSuite) TestStartFailsIfGetLevelsFails(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Levels = &brokenLevelMap{}
sess.Connection = &testConn{Name: "TestStartConnectMessageFails"}
errCh := make(chan error, 1)
upCh := make(chan interface{}, 5)
downCh := make(chan interface{}, 5)
proto := &testProtocol{up: upCh, down: downCh}
sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto }
go func() {
errCh <- sess.start()
}()
c.Check(takeNext(downCh), Equals, "deadline 0")
err = <-errCh
c.Check(err, ErrorMatches, "broken.")
}
func (cs *clientSessionSuite) TestStartConnectMessageFails(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Connection = &testConn{Name: "TestStartConnectMessageFails"}
errCh := make(chan error, 1)
upCh := make(chan interface{}, 5)
downCh := make(chan interface{}, 5)
proto := &testProtocol{up: upCh, down: downCh}
sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto }
go func() {
errCh <- sess.start()
}()
c.Check(takeNext(downCh), Equals, "deadline 0")
c.Check(takeNext(downCh), DeepEquals, protocol.ConnectMsg{
Type: "connect",
DeviceId: sess.DeviceId,
Levels: map[string]int64{},
})
upCh <- errors.New("Overflow error in /dev/null")
err = <-errCh
c.Check(err, ErrorMatches, "Overflow.*null")
c.Check(sess.State(), Equals, Error)
}
func (cs *clientSessionSuite) TestStartConnackReadError(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Connection = &testConn{Name: "TestStartConnackReadError"}
errCh := make(chan error, 1)
upCh := make(chan interface{}, 5)
downCh := make(chan interface{}, 5)
proto := &testProtocol{up: upCh, down: downCh}
sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto }
go func() {
errCh <- sess.start()
}()
c.Check(takeNext(downCh), Equals, "deadline 0")
_, ok := takeNext(downCh).(protocol.ConnectMsg)
c.Check(ok, Equals, true)
upCh <- nil // no error
upCh <- io.EOF
err = <-errCh
c.Check(err, ErrorMatches, ".*EOF.*")
c.Check(sess.State(), Equals, Error)
}
func (cs *clientSessionSuite) TestStartBadConnack(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Connection = &testConn{Name: "TestStartBadConnack"}
errCh := make(chan error, 1)
upCh := make(chan interface{}, 5)
downCh := make(chan interface{}, 5)
proto := &testProtocol{up: upCh, down: downCh}
sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto }
go func() {
errCh <- sess.start()
}()
c.Check(takeNext(downCh), Equals, "deadline 0")
_, ok := takeNext(downCh).(protocol.ConnectMsg)
c.Check(ok, Equals, true)
upCh <- nil // no error
upCh <- protocol.ConnAckMsg{Type: "connack"}
err = <-errCh
c.Check(err, ErrorMatches, ".*invalid.*")
c.Check(sess.State(), Equals, Error)
}
func (cs *clientSessionSuite) TestStartNotConnack(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Connection = &testConn{Name: "TestStartBadConnack"}
errCh := make(chan error, 1)
upCh := make(chan interface{}, 5)
downCh := make(chan interface{}, 5)
proto := &testProtocol{up: upCh, down: downCh}
sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto }
go func() {
errCh <- sess.start()
}()
c.Check(takeNext(downCh), Equals, "deadline 0")
_, ok := takeNext(downCh).(protocol.ConnectMsg)
c.Check(ok, Equals, true)
upCh <- nil // no error
upCh <- protocol.ConnAckMsg{Type: "connnak"}
err = <-errCh
c.Check(err, ErrorMatches, ".*CONNACK.*")
c.Check(sess.State(), Equals, Error)
}
func (cs *clientSessionSuite) TestStartWorks(c *C) {
info := map[string]interface{}{
"foo": 1,
"bar": "baz",
}
conf := ClientSessionConfig{
Info: info,
}
sess, err := NewSession("", conf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Connection = &testConn{Name: "TestStartWorks"}
errCh := make(chan error, 1)
upCh := make(chan interface{}, 5)
downCh := make(chan interface{}, 5)
proto := &testProtocol{up: upCh, down: downCh}
sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto }
go func() {
errCh <- sess.start()
}()
c.Check(takeNext(downCh), Equals, "deadline 0")
msg, ok := takeNext(downCh).(protocol.ConnectMsg)
c.Check(ok, Equals, true)
c.Check(msg.DeviceId, Equals, "wah")
c.Check(msg.Info, DeepEquals, info)
upCh <- nil // no error
upCh <- protocol.ConnAckMsg{
Type: "connack",
Params: protocol.ConnAckParams{(10 * time.Millisecond).String()},
}
// start is now done.
err = <-errCh
c.Check(err, IsNil)
c.Check(sess.State(), Equals, Started)
}
/****************************************************************
run() tests
****************************************************************/
func (cs *clientSessionSuite) TestRunBailsIfHostGetterFails(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
failure := errors.New("TestRunBailsIfHostGetterFails")
has_closed := false
err = sess.run(
func() { has_closed = true },
func() error { return failure },
nil,
nil,
nil)
c.Check(err, Equals, failure)
c.Check(has_closed, Equals, true)
}
func (cs *clientSessionSuite) TestRunBailsIfConnectFails(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
failure := errors.New("TestRunBailsIfConnectFails")
err = sess.run(
func() {},
func() error { return nil },
func() error { return failure },
nil,
nil)
c.Check(err, Equals, failure)
}
func (cs *clientSessionSuite) TestRunBailsIfStartFails(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
failure := errors.New("TestRunBailsIfStartFails")
err = sess.run(
func() {},
func() error { return nil },
func() error { return nil },
func() error { return failure },
nil)
c.Check(err, Equals, failure)
}
func (cs *clientSessionSuite) TestRunRunsEvenIfLoopFails(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
// just to make a point: until here we haven't set ErrCh & MsgCh (no
// biggie if this stops being true)
c.Check(sess.ErrCh, IsNil)
c.Check(sess.MsgCh, IsNil)
failureCh := make(chan error) // must be unbuffered
notf := &Notification{}
err = sess.run(
func() {},
func() error { return nil },
func() error { return nil },
func() error { return nil },
func() error { sess.MsgCh <- notf; return <-failureCh })
c.Check(err, Equals, nil)
// if run doesn't error it sets up the channels
c.Assert(sess.ErrCh, NotNil)
c.Assert(sess.MsgCh, NotNil)
c.Check(<-sess.MsgCh, Equals, notf)
failure := errors.New("TestRunRunsEvenIfLoopFails")
failureCh <- failure
c.Check(<-sess.ErrCh, Equals, failure)
// so now you know it was running in a goroutine :)
}
/****************************************************************
Jitter() tests
****************************************************************/
func (cs *clientSessionSuite) TestJitter(c *C) {
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
num_tries := 20 // should do the math
spread := time.Second //
has_neg := false
has_pos := false
has_zero := true
for i := 0; i < num_tries; i++ {
n := sess.Jitter(spread)
if n > 0 {
has_pos = true
} else if n < 0 {
has_neg = true
} else {
has_zero = true
}
}
c.Check(has_neg, Equals, true)
c.Check(has_pos, Equals, true)
c.Check(has_zero, Equals, true)
// a negative spread is caught in the reasonable place
c.Check(func() { sess.Jitter(time.Duration(-1)) }, PanicMatches,
"spread must be non-negative")
}
/****************************************************************
Dial() tests
****************************************************************/
func (cs *clientSessionSuite) TestDialPanics(c *C) {
// one last unhappy test
sess, err := NewSession("", dummyConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
sess.Protocolator = nil
c.Check(sess.Dial, PanicMatches, ".*protocol constructor.")
}
var (
dialTestTimeout = 100 * time.Millisecond
dialTestConf = ClientSessionConfig{ExchangeTimeout: dialTestTimeout}
)
func (cs *clientSessionSuite) TestDialWorks(c *C) {
// happy path thoughts
cert, err := tls.X509KeyPair(helpers.TestCertPEMBlock, helpers.TestKeyPEMBlock)
c.Assert(err, IsNil)
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
SessionTicketsDisabled: true,
}
lst, err := tls.Listen("tcp", "localhost:0", tlsCfg)
c.Assert(err, IsNil)
// advertise
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, err := json.Marshal(map[string]interface{}{
"hosts": []string{"nowhere", lst.Addr().String()},
})
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(b)
}))
defer ts.Close()
sess, err := NewSession(ts.URL, dialTestConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
tconn := &testConn{CloseCondition: condition.Fail2Work(10)}
sess.Connection = tconn
// just to be sure:
c.Check(tconn.CloseCondition.String(), Matches, ".* 10 to go.")
upCh := make(chan interface{}, 5)
downCh := make(chan interface{}, 5)
proto := &testProtocol{up: upCh, down: downCh}
sess.Protocolator = func(net.Conn) protocol.Protocol { return proto }
go sess.Dial()
srv, err := lst.Accept()
c.Assert(err, IsNil)
// connect done
// Dial should have had the session's old connection (tconn) closed
// before connecting a new one; if that was done, tconn's condition
// ticked forward:
c.Check(tconn.CloseCondition.String(), Matches, ".* 9 to go.")
// now, start: 1. protocol version
v, err := protocol.ReadWireFormatVersion(srv, dialTestTimeout)
c.Assert(err, IsNil)
c.Assert(v, Equals, protocol.ProtocolWireVersion)
// if something goes wrong session would try the first/other host
c.Check(sess.tryHost, Equals, 0)
// 2. "connect" (but on the fake protcol above! woo)
c.Check(takeNext(downCh), Equals, "deadline 100ms")
_, ok := takeNext(downCh).(protocol.ConnectMsg)
c.Check(ok, Equals, true)
upCh <- nil // no error
upCh <- protocol.ConnAckMsg{
Type: "connack",
Params: protocol.ConnAckParams{(10 * time.Millisecond).String()},
}
// start is now done.
// 3. "loop"
// ping works,
c.Check(takeNext(downCh), Equals, "deadline 110ms")
upCh <- protocol.PingPongMsg{Type: "ping"}
c.Check(takeNext(downCh), Equals, protocol.PingPongMsg{Type: "pong"})
upCh <- nil
// session would retry the same host
c.Check(sess.tryHost, Equals, 1)
// and broadcasts...
b := &protocol.BroadcastMsg{
Type: "broadcast",
AppId: "--ignored--",
ChanId: "0",
TopLevel: 2,
Payloads: []json.RawMessage{json.RawMessage(`{"b":1}`)},
}
c.Check(takeNext(downCh), Equals, "deadline 110ms")
upCh <- b
c.Check(takeNext(downCh), Equals, protocol.AckMsg{"ack"})
upCh <- nil
// ...get bubbled up,
c.Check(<-sess.MsgCh, NotNil)
// and their TopLevel remembered
levels, err := sess.Levels.GetAll()
c.Check(err, IsNil)
c.Check(levels, DeepEquals, map[string]int64{"0": 2})
// and ping still work even after that.
c.Check(takeNext(downCh), Equals, "deadline 110ms")
upCh <- protocol.PingPongMsg{Type: "ping"}
c.Check(takeNext(downCh), Equals, protocol.PingPongMsg{Type: "pong"})
failure := errors.New("pongs")
upCh <- failure
c.Check(<-sess.ErrCh, Equals, failure)
}
func (cs *clientSessionSuite) TestDialWorksDirect(c *C) {
// happy path thoughts
cert, err := tls.X509KeyPair(helpers.TestCertPEMBlock, helpers.TestKeyPEMBlock)
c.Assert(err, IsNil)
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
SessionTicketsDisabled: true,
}
lst, err := tls.Listen("tcp", "localhost:0", tlsCfg)
c.Assert(err, IsNil)
sess, err := NewSession(lst.Addr().String(), dialTestConf, "wah", cs.lvls, cs.log)
c.Assert(err, IsNil)
defer sess.Close()
upCh := make(chan interface{}, 5)
downCh := make(chan interface{}, 5)
proto := &testProtocol{up: upCh, down: downCh}
sess.Protocolator = func(net.Conn) protocol.Protocol { return proto }
go sess.Dial()
_, err = lst.Accept()
c.Assert(err, IsNil)
// connect done
}
ubuntu-push-0.2+14.04.20140411/.precommit 0000755 0000153 0177776 00000001701 12322032412 020235 0 ustar pbuser nogroup 0000000 0000000 #!/bin/sh
set -e
echo "$@"
# put me in the project root, call me ".precommit".
# And put this here-document in ~/.bazaar/plugins/precommit_script.py:
<