ubuntu-push-0.68+16.04.20160310.2/ 0000755 0000156 0000165 00000000000 12670364532 016453 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/scripts/ 0000755 0000156 0000165 00000000000 12670364532 020142 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/scripts/check_fmt 0000755 0000156 0000165 00000000763 12670364255 022023 0 ustar pbuser pbgroup 0000000 0000000 #!/bin/bash
# Checks that all Go files in the specified project respect gofmt formatting.
# Requires GOPATH to be set to a single path, not a list of them.
PROJECT=${1:?missing project}
PROBLEMS=
for pkg in $(go list ${PROJECT}/...) ; do
NONCOMPLIANT=$(gofmt -l ${GOPATH}/src/${pkg}/*.go)
if [ -n "${NONCOMPLIANT}" ]; then
echo pkg $pkg has some gofmt non-compliant files:
echo ${NONCOMPLIANT}|xargs -d ' ' -n1 basename
PROBLEMS="y"
fi
done
test -z "${PROBLEMS}"
ubuntu-push-0.68+16.04.20160310.2/scripts/connect-many.py 0000755 0000156 0000165 00000001235 12670364255 023115 0 ustar pbuser pbgroup 0000000 0000000 #!/usr/bin/python3
import sys
import resource
import socket
import ssl
import time
host, port = sys.argv[1].split(":")
addr = (host, int(port))
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
# reset soft == hard
resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
conns = []
t0 = time.time()
try:
for i in range(soft+100):
s=socket.socket()
w = ssl.wrap_socket(s, ssl_version=ssl.PROTOCOL_TLSv1)
w.settimeout(1)
w.connect(addr)
conns.append(w)
w.send(b"x")
except Exception as e:
print("%s|%d|%s" % (e, len(conns), time.time()-t0))
sys.exit(0)
print("UNTROUBLED|%d" % len(conns))
sys.exit(1)
ubuntu-push-0.68+16.04.20160310.2/scripts/dummyauth.sh 0000755 0000156 0000165 00000000110 12670364255 022510 0 ustar pbuser pbgroup 0000000 0000000 #!/bin/sh
# a very dumb "auth helper", for use in tests
echo hello "$@"
ubuntu-push-0.68+16.04.20160310.2/scripts/trivial-helper.sh 0000755 0000156 0000165 00000000040 12670364255 023424 0 ustar pbuser pbgroup 0000000 0000000 #!/bin/sh
set -eu
cat < $1 > $2
ubuntu-push-0.68+16.04.20160310.2/scripts/noisy-helper.sh 0000755 0000156 0000165 00000000114 12670364255 023115 0 ustar pbuser pbgroup 0000000 0000000 #!/bin/sh
for a in `seq 1 100`
do
echo BOOM-$a
>&2 echo BANG-$a
done
exit 1
ubuntu-push-0.68+16.04.20160310.2/scripts/goctest 0000755 0000156 0000165 00000003003 12670364255 021536 0 ustar pbuser pbgroup 0000000 0000000 #!/usr/bin/python3
# -*- python -*-
# (c) 2014 John Lenton
# MIT licensed.
# from https://github.com/chipaca/goctest
import re
import signal
import subprocess
import sys
ok_rx = re.compile(rb'^(PASS:?|ok\s+)')
fail_rx = re.compile(rb'^(FAIL:?|OOPS:?)')
panic_rx = re.compile(rb'^(PANIC:?|panic:?|\.\.\. Panic:?)')
log_rx = re.compile(rb'^\[LOG\]|^\?\s+')
class bcolors:
OK = b'\033[38;5;34m'
FAIL = b'\033[38;5;196m'
PANIC = b'\033[38;5;226m\033[48;5;88m'
OTHER = b'\033[38;5;241m'
WARNING = b'\033[38;5;226m'
ENDC = b'\033[0m'
signal.signal(signal.SIGINT, lambda *_: None)
if sys.stdout.isatty():
with subprocess.Popen(["go", "test"] + sys.argv[1:],
bufsize=0,
stderr=subprocess.STDOUT,
stdout=subprocess.PIPE) as proc:
for line in proc.stdout:
if panic_rx.search(line) is not None:
line = panic_rx.sub(bcolors.PANIC + rb'\1' + bcolors.ENDC, line)
elif fail_rx.search(line) is not None:
line = fail_rx.sub(bcolors.FAIL + rb'\1' + bcolors.ENDC, line)
elif ok_rx.search(line) is not None:
line = ok_rx.sub(bcolors.OK + rb'\1' + bcolors.ENDC, line)
elif log_rx.search(line) is not None:
line = bcolors.OTHER + line + bcolors.ENDC
sys.stdout.write(line.decode("utf-8"))
sys.stdout.flush()
sys.exit(proc.wait())
else:
sys.exit(subprocess.call(["go", "test"] + sys.argv[1:]))
ubuntu-push-0.68+16.04.20160310.2/scripts/register 0000755 0000156 0000165 00000003204 12670364255 021715 0 ustar pbuser pbgroup 0000000 0000000 #!/usr/bin/python3
"""
request a unicast registration
"""
import argparse
import json
import requests
import subprocess
import datetime
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('deviceid', nargs=1)
parser.add_argument('appid', nargs=1)
parser.add_argument('-H', '--host',
help="host:port (default: %(default)s)",
default="localhost:8080")
parser.add_argument('--no-https', action='store_true', default=False)
parser.add_argument('--insecure', action='store_true', default=False,
help="don't check host/certs with https")
parser.add_argument('--auth_helper', default="")
parser.add_argument('--unregister', action='store_true', default=False)
args = parser.parse_args()
scheme = 'https'
if args.no_https:
scheme = 'http'
if args.unregister:
op = 'unregister'
else:
op = 'register'
url = "%s://%s/%s" % (scheme, args.host, op)
body = {
'deviceid': args.deviceid[0],
'appid': args.appid[0],
}
headers = {'Content-Type': 'application/json'}
if args.auth_helper:
auth = subprocess.check_output([args.auth_helper, url]).strip()
headers['Authorization'] = auth
r = requests.post(url, data=json.dumps(body), headers=headers,
verify=not args.insecure)
if r.status_code == 200 and not args.unregister:
print(r.json()['token'])
else:
print(r.status_code)
print(r.text)
if r.status_code != 200:
sys.exit(1)
if __name__ == '__main__':
main()
ubuntu-push-0.68+16.04.20160310.2/scripts/click-hook 0000755 0000156 0000165 00000010041 12670364255 022111 0 ustar pbuser pbgroup 0000000 0000000 #!/usr/bin/python3
# -*- python -*-
"""Collect helpers hook data into a single json file"""
import argparse
import json
import os
import sys
import time
import xdg.BaseDirectory
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Click
hook_ext = '.json'
def tup2variant(tup):
builder = GLib.VariantBuilder.new(GLib.VariantType.new("(ss)"))
builder.add_value(GLib.Variant.new_string(tup[0]))
builder.add_value(GLib.Variant.new_string(tup[1]))
return builder.end()
def cleanup_settings():
settings = Gio.Settings.new('com.ubuntu.notifications.hub')
blacklist = settings.get_value('blacklist').unpack()
if not blacklist:
return
clickdb = Click.DB.new()
clickdb.read()
pkgnames = set()
for package in clickdb.get_packages(False):
pkgnames.add(package.get_property('package'))
goodapps = GLib.VariantBuilder.new(GLib.VariantType.new("a(ss)"))
dirty = False
for appname in blacklist:
if appname[0] not in pkgnames and Gio.DesktopAppInfo.new(appname[1] + ".desktop") is None:
dirty = True
else:
goodapps.add_value(tup2variant(appname))
if dirty:
settings.set_value('blacklist', goodapps.end())
def collect_helpers(helpers_data_path, helpers_data_path_tmp, hooks_path):
helpers_data = {}
if not os.path.isdir(hooks_path):
return True
for hook_fname in os.listdir(hooks_path):
if not hook_fname.endswith(hook_ext):
continue
try:
with open(os.path.join(hooks_path, hook_fname), 'r') as fd:
data = json.load(fd)
except Exception:
continue
else:
helper_id = os.path.splitext(hook_fname)[0]
exec_path = data['exec']
if exec_path != "":
realpath = os.path.realpath(os.path.join(hooks_path,
hook_fname))
exec_path = os.path.join(os.path.dirname(realpath), exec_path)
app_id = data.get('app_id', None)
if app_id is None:
# no app_id, use the package name from the helper_id
app_id = helper_id.split('_')[0]
elif app_id.count('_') >= 3:
# remove the version from the app_id
app_id = app_id.rsplit('_', 1)[0]
helpers_data[app_id] = {'exec': exec_path, 'helper_id': helper_id}
# write the collected data to a temp file and rename the original once
# everything is on disk
try:
tmp_filename = helpers_data_path_tmp % (time.time(),)
with open(tmp_filename, 'w') as dest:
json.dump(helpers_data, dest)
dest.flush()
os.rename(tmp_filename, helpers_data_path)
except Exception:
return True
return False
def main(helpers_data_path=None, helpers_data_path_tmp=None, hooks_path=None):
collect_fail = collect_helpers(helpers_data_path, helpers_data_path_tmp,
hooks_path)
clean_settings_fail = False
try:
cleanup_settings()
except Exception:
clean_settings_fail = True
return int(collect_fail or clean_settings_fail)
if __name__ == "__main__":
xdg_data_home = xdg.BaseDirectory.xdg_data_home
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-d', '--data-home',
help='The Path to the (xdg) data home',
default=xdg_data_home)
args = parser.parse_args()
xdg_data_home = args.data_home
helpers_data_path = os.path.join(xdg_data_home, 'ubuntu-push-client',
'helpers_data.json')
helpers_data_path_tmp = os.path.join(xdg_data_home, 'ubuntu-push-client',
'.helpers_data_%s.tmp')
hooks_path = os.path.join(xdg_data_home, 'ubuntu-push-client', 'helpers')
sys.exit(main(helpers_data_path=helpers_data_path,
helpers_data_path_tmp=helpers_data_path_tmp,
hooks_path=hooks_path))
ubuntu-push-0.68+16.04.20160310.2/scripts/deps.sh 0000755 0000156 0000165 00000001011 12670364255 021427 0 ustar pbuser pbgroup 0000000 0000000 #!/bin/sh
set -eu
PROJECT=launchpad.net/ubuntu-push
mktpl () {
for f in GoFiles CgoFiles; do
echo '{{join .'$f' "\\n"}}'
done
}
directs () {
go list -f "$(mktpl)" $1 | sed -e "s|^|$1|"
}
indirects () {
for i in $(go list -f '{{join .Deps "\n"}}' $1 | grep ^$PROJECT ); do
directs $i/
done
wait
}
norm () {
tr "\n" " " | sed -r -e "s|$PROJECT/?||g" -e 's/ *$//'
}
out="$1.deps"
( echo -n "${1%.go} ${out} dependencies.tsv: "; indirects $(echo $1 | norm) | norm ) > "$out"
ubuntu-push-0.68+16.04.20160310.2/scripts/broadcast 0000755 0000156 0000165 00000004051 12670364255 022034 0 ustar pbuser pbgroup 0000000 0000000 #!/usr/bin/python
"""
send broadcast to channel with payload data
"""
import argparse
import json
import requests
import requests.auth
import datetime
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('channel', nargs=1)
parser.add_argument('data', nargs=1)
parser.add_argument('-H', '--host',
help="host:port (default: %(default)s)",
default="localhost:8080")
parser.add_argument('-e', '--expire',
help="expire after the given amount of time, "
"use 'd' suffix for days, 's' for seconds"
" (default: %(default)s)", default="1d")
parser.add_argument('--no-https', action='store_true', default=False)
parser.add_argument('--insecure', action='store_true', default=False,
help="don't check host/certs with https")
parser.add_argument('-u', '--user', default="")
parser.add_argument('-p', '--password', default="")
args = parser.parse_args()
expire_on = datetime.datetime.utcnow()
ex = args.expire
if ex.endswith('d'):
delta = datetime.timedelta(days=int(ex[:-1]))
elif ex.endswith('s'):
delta = datetime.timedelta(seconds=int(ex[:-1]))
else:
print >>sys.stderr, "unknown --expire suffix:", ex
sys.exit(1)
expire_on += delta
scheme = 'https'
if args.no_https:
scheme = 'http'
url = "%s://%s/broadcast" % (scheme, args.host)
body = {
'channel': args.channel[0],
'data': json.loads(args.data[0]),
'expire_on': expire_on.replace(microsecond=0).isoformat()+"Z"
}
xauth = {}
if args.user and args.password:
xauth = {'auth': requests.auth.HTTPBasicAuth(args.user, args.password)}
headers = {'Content-Type': 'application/json'}
r = requests.post(url, data=json.dumps(body), headers=headers,
verify=not args.insecure, **xauth)
print r.status_code
print r.text
if __name__ == '__main__':
main()
ubuntu-push-0.68+16.04.20160310.2/scripts/unicast 0000755 0000156 0000165 00000004425 12670364255 021545 0 ustar pbuser pbgroup 0000000 0000000 #!/usr/bin/python
"""
send unicast to reg with payload data
"""
import argparse
import json
import requests
import requests.auth
import datetime
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('reg', nargs=1) # userid:deviceid or reg
parser.add_argument('appid', nargs=1)
parser.add_argument('data', nargs=1)
parser.add_argument('-H', '--host',
help="host:port (default: %(default)s)",
default="localhost:8080")
parser.add_argument('-e', '--expire',
help="expire after the given amount of time, "
"use 'd' suffix for days, 's' for seconds"
" (default: %(default)s)", default="1d")
parser.add_argument('--no-https', action='store_true', default=False)
parser.add_argument('--insecure', action='store_true', default=False,
help="don't check host/certs with https")
parser.add_argument('-u', '--user', default="")
parser.add_argument('-p', '--password', default="")
args = parser.parse_args()
expire_on = datetime.datetime.utcnow()
ex = args.expire
if ex.endswith('d'):
delta = datetime.timedelta(days=int(ex[:-1]))
elif ex.endswith('s'):
delta = datetime.timedelta(seconds=int(ex[:-1]))
else:
print >>sys.stderr, "unknown --expire suffix:", ex
sys.exit(1)
expire_on += delta
scheme = 'https'
if args.no_https:
scheme = 'http'
url = "%s://%s/notify" % (scheme, args.host)
body = {
'appid': args.appid[0],
'data': json.loads(args.data[0]),
'expire_on': expire_on.replace(microsecond=0).isoformat()+"Z"
}
reg = args.reg[0]
if ':' in reg:
userid, devid = reg.split(':', 1)
body['userid'] = userid
body['deviceid'] = devid
else:
body['token'] = reg
xauth = {}
if args.user and args.password:
xauth = {'auth': requests.auth.HTTPBasicAuth(args.user, args.password)}
headers = {'Content-Type': 'application/json'}
r = requests.post(url, data=json.dumps(body), headers=headers,
verify=not args.insecure, **xauth)
print r.status_code
print r.text
if __name__ == '__main__':
main()
ubuntu-push-0.68+16.04.20160310.2/scripts/slow-helper.sh 0000755 0000156 0000165 00000000023 12670364255 022737 0 ustar pbuser pbgroup 0000000 0000000 #!/bin/sh
sleep 10
ubuntu-push-0.68+16.04.20160310.2/sampleconfigs/ 0000755 0000156 0000165 00000000000 12670364532 021305 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/sampleconfigs/dev.json 0000644 0000156 0000165 00000000702 12670364255 022757 0 ustar pbuser pbgroup 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",
"max_notifications_per_app": 25,
"delivery_domain": "push-delivery"
}
ubuntu-push-0.68+16.04.20160310.2/urldispatcher/ 0000755 0000156 0000165 00000000000 12670364532 021324 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/urldispatcher/curldispatcher/ 0000755 0000156 0000165 00000000000 12670364532 024340 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/urldispatcher/curldispatcher/curldispatcher_c.go 0000644 0000156 0000165 00000002435 12670364255 030213 0 ustar pbuser pbgroup 0000000 0000000 /*
Copyright 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 curldispatcher wraps liburl-dispatch1
package curldispatcher
/*
#cgo pkg-config: url-dispatcher-1
#include
#include
char* handleDispatchURLResult(const gchar * url, gboolean success, gpointer user_data);
static void url_dispatch_callback(const gchar * url, gboolean success, gpointer user_data) {
handleDispatchURLResult(url, success, user_data);
}
void dispatch_url(const gchar* url, gpointer user_data) {
url_dispatch_send(url, (URLDispatchCallback)url_dispatch_callback, user_data);
}
gchar** test_url(const gchar** urls) {
char** result = url_dispatch_url_appid(urls);
return result;
}
*/
import "C"
ubuntu-push-0.68+16.04.20160310.2/urldispatcher/curldispatcher/curldispatcher.go 0000644 0000156 0000165 00000004574 12670364255 027717 0 ustar pbuser pbgroup 0000000 0000000 /*
Copyright 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 cmessaging wraps libmessaging-menu
package curldispatcher
/*
#cgo pkg-config: url-dispatcher-1
#include
#include
void dispatch_url(const gchar* url, gpointer user_data);
gchar** test_url(const gchar** urls);
*/
import "C"
import "unsafe"
import "fmt"
func gchar(s string) *C.gchar {
return (*C.gchar)(C.CString(s))
}
func gfree(s *C.gchar) {
C.g_free((C.gpointer)(s))
}
func getCharPtr(p uintptr) *C.char {
return *((**C.char)(unsafe.Pointer(p)))
}
func TestURL(urls []string) []string {
c_urls := make([]*C.gchar, len(urls)+1)
for i, url := range urls {
c_urls[i] = gchar(url)
defer gfree(c_urls[i])
}
results := C.test_url((**C.gchar)(unsafe.Pointer(&c_urls[0])))
// if there result is nil, just return empty []string
if results == nil {
return nil
}
packages := make([]string, len(urls))
ptrSz := unsafe.Sizeof(unsafe.Pointer(nil))
i := 0
for p := uintptr(unsafe.Pointer(results)); getCharPtr(p) != nil; p += ptrSz {
pkg := C.GoString(getCharPtr(p))
packages[i] = pkg
i += 1
}
return packages
}
type DispatchPayload struct {
doneCh chan bool
}
func DispatchURL(url string, appPackage string) error {
c_url := gchar(url)
defer gfree(c_url)
c_app_package := gchar(appPackage)
defer gfree(c_app_package)
doneCh := make(chan bool)
payload := DispatchPayload{doneCh: doneCh}
C.dispatch_url(c_url, (C.gpointer)(&payload))
success := <-doneCh
if !success {
return fmt.Errorf("failed to DispatchURL: %s for %s", url, appPackage)
}
return nil
}
//export handleDispatchURLResult
func handleDispatchURLResult(c_action *C.char, c_success C.gboolean, obj unsafe.Pointer) {
payload := (*DispatchPayload)(obj)
success := false
if c_success == C.TRUE {
success = true
}
payload.doneCh <- success
}
ubuntu-push-0.68+16.04.20160310.2/urldispatcher/urldispatcher.go 0000644 0000156 0000165 00000004034 12670364255 024527 0 ustar pbuser pbgroup 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 urldispatcher wraps the url dispatcher's C API
package urldispatcher
import (
"launchpad.net/ubuntu-push/click"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/urldispatcher/curldispatcher"
)
// A URLDispatcher wrapper.
type URLDispatcher interface {
DispatchURL(string, *click.AppId) error
TestURL(*click.AppId, []string) bool
}
type urlDispatcher struct {
log logger.Logger
}
// New builds a new URL dispatcher that uses the provided bus.Endpoint
func New(log logger.Logger) URLDispatcher {
return &urlDispatcher{log}
}
var _ URLDispatcher = &urlDispatcher{} // ensures it conforms
var cDispatchURL = curldispatcher.DispatchURL
var cTestURL = curldispatcher.TestURL
func (ud *urlDispatcher) DispatchURL(url string, app *click.AppId) error {
ud.log.Debugf("dispatching %s", url)
err := cDispatchURL(url, app.DispatchPackage())
if err != nil {
ud.log.Errorf("DispatchURL failed: %s", err)
}
return err
}
func (ud *urlDispatcher) TestURL(app *click.AppId, urls []string) bool {
ud.log.Debugf("TestURL: %s", urls)
var appIds []string
appIds = cTestURL(urls)
if len(appIds) == 0 {
ud.log.Debugf("TestURL: invalid urls: %s - %s", urls, app.Versioned())
return false
}
for _, appId := range appIds {
if appId != app.Versioned() {
ud.log.Debugf("notification skipped because of different appid for actions: %v - %s != %s", urls, appId, app.Versioned())
return false
}
}
return true
}
ubuntu-push-0.68+16.04.20160310.2/urldispatcher/urldispatcher_test.go 0000644 0000156 0000165 00000011134 12670364255 025565 0 ustar pbuser pbgroup 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 urldispatcher
import (
"errors"
. "launchpad.net/gocheck"
clickhelp "launchpad.net/ubuntu-push/click/testing"
helpers "launchpad.net/ubuntu-push/testing"
"testing"
)
// hook up gocheck
func TestUrldispatcher(t *testing.T) { TestingT(t) }
type UDSuite struct {
log *helpers.TestLogger
cDispatchURL func(string, string) error
cTestURL func([]string) []string
}
var _ = Suite(&UDSuite{})
func (s *UDSuite) SetUpTest(c *C) {
s.log = helpers.NewTestLogger(c, "debug")
s.cDispatchURL = cDispatchURL
s.cTestURL = cTestURL
// replace it with a always succeed version
cDispatchURL = func(url string, appId string) error {
return nil
}
}
func (s *UDSuite) TearDownTest(c *C) {
cDispatchURL = s.cDispatchURL
cTestURL = s.cTestURL
}
func (s *UDSuite) TestDispatchURLWorks(c *C) {
ud := New(s.log)
appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
err := ud.DispatchURL("this", appId)
c.Check(err, IsNil)
}
func (s *UDSuite) TestDispatchURLFailsIfCallFails(c *C) {
cDispatchURL = func(url string, appId string) error {
return errors.New("fail!")
}
ud := New(s.log)
appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
err := ud.DispatchURL("this", appId)
c.Check(err, NotNil)
}
func (s *UDSuite) TestTestURLWorks(c *C) {
cTestURL = func(url []string) []string {
return []string{"com.example.test_app_0.99"}
}
ud := New(s.log)
appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
c.Check(ud.TestURL(appId, []string{"this"}), Equals, true)
c.Check(s.log.Captured(), Matches, `(?sm).*TestURL: \[this\].*`)
}
func (s *UDSuite) TestTestURLFailsIfCallFails(c *C) {
cTestURL = func(url []string) []string {
return []string{}
}
ud := New(s.log)
appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
c.Check(ud.TestURL(appId, []string{"this"}), Equals, false)
}
func (s *UDSuite) TestTestURLMultipleURLs(c *C) {
cTestURL = func(url []string) []string {
return []string{"com.example.test_app_0.99", "com.example.test_app_0.99"}
}
ud := New(s.log)
appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
urls := []string{"potato://test-app", "potato_a://foo"}
c.Check(ud.TestURL(appId, urls), Equals, true)
c.Check(s.log.Captured(), Matches, `(?sm).*TestURL: \[potato://test-app potato_a://foo\].*`)
}
func (s *UDSuite) TestTestURLWrongApp(c *C) {
cTestURL = func(url []string) []string {
return []string{"com.example.test_test-app_0.1"}
}
ud := New(s.log)
appId := clickhelp.MustParseAppId("com.example.test_app_0.99")
urls := []string{"potato://test-app"}
c.Check(ud.TestURL(appId, urls), Equals, false)
c.Check(s.log.Captured(), Matches, `(?smi).*notification skipped because of different appid for actions: \[potato://test-app\] - com.example.test_test-app_0.1 != com.example.test_app_0.99`)
}
func (s *UDSuite) TestTestURLOneWrongApp(c *C) {
cTestURL = func(url []string) []string {
return []string{"com.example.test_test-app_0", "com.example.test_test-app1"}
}
ud := New(s.log)
appId := clickhelp.MustParseAppId("com.example.test_test-app_0")
urls := []string{"potato://test-app", "potato_a://foo"}
c.Check(ud.TestURL(appId, urls), Equals, false)
c.Check(s.log.Captured(), Matches, `(?smi).*notification skipped because of different appid for actions: \[potato://test-app potato_a://foo\] - com.example.test_test-app1 != com.example.test_test-app.*`)
}
func (s *UDSuite) TestTestURLInvalidURL(c *C) {
cTestURL = func(url []string) []string {
return []string{}
}
ud := New(s.log)
appId := clickhelp.MustParseAppId("com.example.test_app_0.2")
urls := []string{"notsupported://test-app"}
c.Check(ud.TestURL(appId, urls), Equals, false)
}
func (s *UDSuite) TestTestURLLegacyApp(c *C) {
cTestURL = func(url []string) []string {
return []string{"ubuntu-system-settings"}
}
ud := New(s.log)
appId := clickhelp.MustParseAppId("_ubuntu-system-settings")
urls := []string{"settings://test-app"}
c.Check(ud.TestURL(appId, urls), Equals, true)
c.Check(s.log.Captured(), Matches, `(?sm).*TestURL: \[settings://test-app\].*`)
}
ubuntu-push-0.68+16.04.20160310.2/server/ 0000755 0000156 0000165 00000000000 12670364532 017761 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/doc.go 0000644 0000156 0000165 00000001335 12670364255 021061 0 ustar pbuser pbgroup 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.68+16.04.20160310.2/server/api/ 0000755 0000156 0000165 00000000000 12670364532 020532 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/api/handlers_test.go 0000644 0000156 0000165 00000105576 12670364255 023740 0 ustar pbuser pbgroup 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/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/protocol"
"launchpad.net/ubuntu-push/server/store"
help "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 *help.TestLogger
}
var _ = Suite(&handlersSuite{})
func (s *handlersSuite) SetUpTest(c *C) {
s.client = &http.Client{}
s.testlog = help.NewTestLogger(c, "error")
}
func (s *handlersSuite) TestAPIError(c *C) {
var apiErr error = &APIError{400, invalidRequest, "Message", nil}
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) TestAPIErrorExtra(c *C) {
apiErr1 := &APIError{400, invalidRequest, "Message", nil}
payload := json.RawMessage(`{"x":1}`)
apiErr := apiErrorWithExtra(apiErr1, &payload)
c.Check(apiErr1.Extra, IsNil)
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","extra":{"x":1}}`)
}
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)
}
type testStoreAccess func(w http.ResponseWriter, request *http.Request) (store.PendingStore, error)
func (tsa testStoreAccess) StoreForRequest(w http.ResponseWriter, request *http.Request) (store.PendingStore, error) {
return tsa(w, request)
}
func (tsa testStoreAccess) GetMaxNotificationsPerApplication() int {
return 4
}
func (s *handlersSuite) TestGetStore(c *C) {
ctx := &context{storage: testStoreAccess(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{storage: testStoreAccess(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) TestCheckCastBroadcastAndCommon(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
notifications []protocol.Notification
meta []store.Metadata
}
func (cbsend *checkBrokerSending) Broadcast(chanId store.InternalChannelId) {
top, notifications, meta, err := cbsend.store.GetChannelUnfiltered(chanId)
cbsend.err = err
cbsend.chanId = chanId
cbsend.top = top
cbsend.notifications = notifications
cbsend.meta = meta
}
func (cbsend *checkBrokerSending) Unicast(chanIds ...store.InternalChannelId) {
// for now
if len(chanIds) != 1 {
panic("not expecting many chan ids for now")
}
cbsend.Broadcast(chanIds[0])
}
func (s *handlersSuite) TestDoBroadcast(c *C) {
sto := store.NewInMemoryPendingStore()
bsend := &checkBrokerSending{store: sto}
ctx := &context{nil, bsend, nil}
payload := json.RawMessage(`{"a": 1}`)
res, apiErr := doBroadcast(ctx, sto, &Broadcast{
Channel: "system",
ExpireOn: future,
Data: payload,
})
c.Assert(apiErr, IsNil)
c.Assert(res, IsNil)
c.Check(bsend.err, IsNil)
c.Check(bsend.chanId, Equals, store.SystemInternalChannelId)
c.Check(bsend.top, Equals, int64(1))
c.Check(bsend.notifications, DeepEquals, help.Ns(payload))
}
func (s *handlersSuite) TestDoBroadcastUnknownChannel(c *C) {
sto := store.NewInMemoryPendingStore()
_, apiErr := doBroadcast(nil, 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) Register(appId, deviceId string) (string, error) {
token, err := isto.InMemoryPendingStore.Register(appId, deviceId)
return token, isto.intercept("Register", err)
}
func (isto *interceptInMemoryPendingStore) Unregister(appId, deviceId string) error {
err := isto.InMemoryPendingStore.Unregister(appId, deviceId)
return isto.intercept("Unregister", err)
}
func (isto *interceptInMemoryPendingStore) GetInternalChannelIdFromToken(token, appId, userId, deviceId string) (store.InternalChannelId, error) {
chanId, err := isto.InMemoryPendingStore.GetInternalChannelIdFromToken(token, appId, userId, deviceId)
return chanId, isto.intercept("GetInternalChannelIdFromToken", err)
}
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 (isto *interceptInMemoryPendingStore) AppendToUnicastChannel(chanId store.InternalChannelId, appId string, payload json.RawMessage, msgId string, meta store.Metadata) error {
err := isto.InMemoryPendingStore.AppendToUnicastChannel(chanId, appId, payload, msgId, meta)
return isto.intercept("AppendToUnicastChannel", err)
}
func (isto *interceptInMemoryPendingStore) GetChannelUnfiltered(chanId store.InternalChannelId) (int64, []protocol.Notification, []store.Metadata, error) {
top, notifs, meta, err := isto.InMemoryPendingStore.GetChannelUnfiltered(chanId)
return top, notifs, meta, isto.intercept("GetChannelUnfiltered", err)
}
func (isto *interceptInMemoryPendingStore) Scrub(chanId store.InternalChannelId, criteria ...string) error {
err := isto.InMemoryPendingStore.Scrub(chanId, criteria...)
return isto.intercept("Scrub", err)
}
func (s *handlersSuite) TestDoBroadcastUnknownError(c *C) {
sto := &interceptInMemoryPendingStore{
store.NewInMemoryPendingStore(),
func(meth string, err error) error {
return errors.New("other")
},
}
_, apiErr := doBroadcast(nil, 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}
_, apiErr := doBroadcast(ctx, 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 (s *handlersSuite) TestCheckUnicast(c *C) {
payload := json.RawMessage(`{"foo":"bar"}`)
unicast := func() *Unicast {
return &Unicast{
UserId: "user1",
DeviceId: "DEV1",
AppId: "app1",
ExpireOn: future,
Data: payload,
}
}
u := unicast()
expire, apiErr := checkUnicast(u)
c.Assert(apiErr, IsNil)
c.Check(expire.Format(time.RFC3339), Equals, future)
u = unicast()
u.UserId = ""
u.DeviceId = ""
u.Token = "TOKEN"
expire, apiErr = checkUnicast(u)
c.Assert(apiErr, IsNil)
c.Check(expire.Format(time.RFC3339), Equals, future)
u = unicast()
u.UserId = ""
expire, apiErr = checkUnicast(u)
c.Check(apiErr, Equals, ErrMissingIdField)
u = unicast()
u.AppId = ""
expire, apiErr = checkUnicast(u)
c.Check(apiErr, Equals, ErrMissingIdField)
u = unicast()
u.DeviceId = ""
expire, apiErr = checkUnicast(u)
c.Check(apiErr, Equals, ErrMissingIdField)
u = unicast()
u.Data = json.RawMessage(nil)
expire, apiErr = checkUnicast(u)
c.Check(apiErr, Equals, ErrMissingData)
u = unicast()
u.Data = json.RawMessage(`{"a":"` + strings.Repeat("x", 2040) + `"}`)
expire, apiErr = checkUnicast(u)
c.Check(apiErr, IsNil)
u = unicast()
u.Data = json.RawMessage(`{"a":"` + strings.Repeat("x", 2041) + `"}`)
expire, apiErr = checkUnicast(u)
c.Check(apiErr, Equals, ErrDataTooLarge)
}
func (s *handlersSuite) TestGenerateMsgId(c *C) {
msgId := generateMsgId()
decoded, err := base64.StdEncoding.DecodeString(msgId)
c.Assert(err, IsNil)
c.Check(decoded, HasLen, 16)
}
func (s *handlersSuite) TestDoUnicast(c *C) {
prevGenMsgId := generateMsgId
defer func() {
generateMsgId = prevGenMsgId
}()
generateMsgId = func() string {
return "MSG-ID"
}
sto := store.NewInMemoryPendingStore()
bsend := &checkBrokerSending{store: sto}
ctx := &context{testStoreAccess(nil), bsend, s.testlog}
payload := json.RawMessage(`{"a": 1}`)
res, apiErr := doUnicast(ctx, sto, &Unicast{
UserId: "user1",
DeviceId: "DEV1",
AppId: "app1",
ExpireOn: future,
Data: payload,
})
c.Assert(apiErr, IsNil)
c.Check(res, IsNil)
c.Check(bsend.err, IsNil)
c.Check(bsend.chanId, Equals, store.UnicastInternalChannelId("user1", "DEV1"))
c.Check(bsend.top, Equals, int64(0))
c.Check(bsend.notifications, DeepEquals, []protocol.Notification{
protocol.Notification{
AppId: "app1",
MsgId: "MSG-ID",
Payload: payload,
},
})
}
func (s *handlersSuite) TestDoUnicastMissingIdField(c *C) {
sto := store.NewInMemoryPendingStore()
_, apiErr := doUnicast(nil, sto, &Unicast{
ExpireOn: future,
Data: json.RawMessage(`{"a": 1}`),
})
c.Check(apiErr, Equals, ErrMissingIdField)
}
func (s *handlersSuite) TestDoUnicastCouldNotStoreNotification(c *C) {
sto := &interceptInMemoryPendingStore{
store.NewInMemoryPendingStore(),
func(meth string, err error) error {
if meth == "AppendToUnicastChannel" {
return errors.New("fail")
}
return err
},
}
ctx := &context{storage: testStoreAccess(nil), logger: s.testlog}
_, apiErr := doUnicast(ctx, sto, &Unicast{
UserId: "user1",
DeviceId: "DEV1",
AppId: "app1",
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 (s *handlersSuite) TestDoUnicastCouldNotPeekAtNotifications(c *C) {
sto := &interceptInMemoryPendingStore{
store.NewInMemoryPendingStore(),
func(meth string, err error) error {
if meth == "GetChannelUnfiltered" {
return errors.New("fail")
}
return err
},
}
ctx := &context{storage: testStoreAccess(nil), logger: s.testlog}
_, apiErr := doUnicast(ctx, sto, &Unicast{
UserId: "user1",
DeviceId: "DEV1",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(`{"a": 1}`),
})
c.Check(apiErr, Equals, ErrCouldNotStoreNotification)
c.Check(s.testlog.Captured(), Equals, "ERROR could not peek at notifications: fail\n")
}
func (s *handlersSuite) TestDoUnicastTooManyNotifications(c *C) {
sto := store.NewInMemoryPendingStore()
chanId := store.UnicastInternalChannelId("user1", "DEV1")
expire := store.Metadata{Expiration: time.Now().Add(4 * time.Hour)}
n1 := json.RawMessage(`{"o":1}`)
n2 := json.RawMessage(`{"o":2}`)
n3 := json.RawMessage(`{"o":3}`)
n4 := json.RawMessage(`{"o":4}`)
sto.AppendToUnicastChannel(chanId, "app1", n1, "m1", expire)
sto.AppendToUnicastChannel(chanId, "app1", n2, "m2", expire)
sto.AppendToUnicastChannel(chanId, "app1", n3, "m3", expire)
sto.AppendToUnicastChannel(chanId, "app1", n4, "m4", expire)
ctx := &context{storage: testStoreAccess(nil), logger: s.testlog}
_, apiErr := doUnicast(ctx, sto, &Unicast{
UserId: "user1",
DeviceId: "DEV1",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(`{"a": 1}`),
})
c.Assert(apiErr, NotNil)
extra := apiErr.Extra
apiErr.Extra = nil
c.Check(apiErr, DeepEquals, ErrTooManyPendingNotifications)
c.Check(extra, DeepEquals, n4)
c.Check(s.testlog.Captured(), Equals, "")
}
func (s *handlersSuite) TestDoUnicastWithScrub(c *C) {
prevGenMsgId := generateMsgId
defer func() {
generateMsgId = prevGenMsgId
}()
generateMsgId = func() string {
return "MSG-ID"
}
sto := store.NewInMemoryPendingStore()
chanId := store.UnicastInternalChannelId("user1", "DEV1")
expire := store.Metadata{Expiration: time.Now().Add(4 * time.Hour)}
old := store.Metadata{Expiration: time.Now().Add(-1 * time.Hour)}
n := json.RawMessage("{}")
sto.AppendToUnicastChannel(chanId, "app1", n, "m1", expire)
sto.AppendToUnicastChannel(chanId, "app1", n, "m2", old)
sto.AppendToUnicastChannel(chanId, "app1", n, "m3", old)
sto.AppendToUnicastChannel(chanId, "app1", n, "m4", expire)
bsend := &checkBrokerSending{store: sto}
ctx := &context{testStoreAccess(nil), bsend, s.testlog}
payload := json.RawMessage(`{"a": 1}`)
res, apiErr := doUnicast(ctx, sto, &Unicast{
UserId: "user1",
DeviceId: "DEV1",
AppId: "app1",
ExpireOn: future,
Data: payload,
})
c.Assert(apiErr, IsNil)
c.Check(res, IsNil)
c.Check(bsend.err, IsNil)
c.Check(bsend.chanId, Equals, store.UnicastInternalChannelId("user1", "DEV1"))
c.Check(bsend.top, Equals, int64(0))
c.Check(bsend.notifications, HasLen, 3)
c.Check(bsend.notifications[0].MsgId, Equals, "m1")
c.Check(bsend.notifications[1].MsgId, Equals, "m4")
c.Check(bsend.notifications[2], DeepEquals, protocol.Notification{
AppId: "app1",
MsgId: "MSG-ID",
Payload: payload,
})
}
func (s *handlersSuite) TestDoUnicastWithReplaceTag(c *C) {
prevGenMsgId := generateMsgId
defer func() {
generateMsgId = prevGenMsgId
}()
m := 0
generateMsgId = func() string {
m++
return fmt.Sprintf("MSG-ID-%d", m)
}
sto := store.NewInMemoryPendingStore()
chanId := store.UnicastInternalChannelId("user1", "DEV1")
expire := store.Metadata{Expiration: time.Now().Add(-1 * time.Hour)}
n := json.RawMessage("{}")
sto.AppendToUnicastChannel(chanId, "app1", n, "m1", expire)
bsend := &checkBrokerSending{store: sto}
ctx := &context{testStoreAccess(nil), bsend, s.testlog}
payload := json.RawMessage(`{"a": 1}`)
res, apiErr := doUnicast(ctx, sto, &Unicast{
UserId: "user1",
DeviceId: "DEV1",
AppId: "app1",
ExpireOn: future,
ReplaceTag: "u1",
Data: payload,
})
c.Assert(apiErr, IsNil)
c.Check(res, IsNil)
c.Check(bsend.err, IsNil)
c.Check(bsend.chanId, Equals, store.UnicastInternalChannelId("user1", "DEV1"))
c.Check(bsend.top, Equals, int64(0))
c.Check(bsend.notifications, HasLen, 1)
c.Check(bsend.notifications[0], DeepEquals, protocol.Notification{
AppId: "app1",
MsgId: "MSG-ID-1",
Payload: payload,
})
futureTime, err := time.Parse(time.RFC3339, future)
c.Assert(err, IsNil)
c.Check(bsend.meta[0], DeepEquals, store.Metadata{
Expiration: futureTime,
ReplaceTag: "u1",
})
// replace
payload2 := json.RawMessage(`{"a": 2}`)
res, apiErr = doUnicast(ctx, sto, &Unicast{
UserId: "user1",
DeviceId: "DEV1",
AppId: "app1",
ExpireOn: future,
ReplaceTag: "u1",
Data: payload2,
})
c.Assert(apiErr, IsNil)
c.Check(res, IsNil)
c.Check(bsend.err, IsNil)
c.Check(bsend.chanId, Equals, store.UnicastInternalChannelId("user1", "DEV1"))
c.Check(bsend.top, Equals, int64(0))
c.Check(bsend.notifications, HasLen, 1)
c.Check(bsend.notifications[0], DeepEquals, protocol.Notification{
AppId: "app1",
MsgId: "MSG-ID-2",
Payload: payload2,
})
c.Assert(err, IsNil)
c.Check(bsend.meta[0], DeepEquals, store.Metadata{
Expiration: futureTime,
ReplaceTag: "u1",
})
}
func (s *handlersSuite) TestDoUnicastWithScrubError(c *C) {
sto := &interceptInMemoryPendingStore{
store.NewInMemoryPendingStore(),
func(meth string, err error) error {
if meth == "Scrub" {
return errors.New("fail")
}
return err
},
}
chanId := store.UnicastInternalChannelId("user1", "DEV1")
expire := store.Metadata{Expiration: time.Now().Add(4 * time.Hour)}
old := store.Metadata{Expiration: time.Now().Add(-1 * time.Hour)}
n := json.RawMessage("{}")
sto.AppendToUnicastChannel(chanId, "app1", n, "m1", expire)
sto.AppendToUnicastChannel(chanId, "app1", n, "m2", old)
sto.AppendToUnicastChannel(chanId, "app1", n, "m3", old)
sto.AppendToUnicastChannel(chanId, "app1", n, "m4", expire)
ctx := &context{testStoreAccess(nil), nil, s.testlog}
payload := json.RawMessage(`{"a": 1}`)
_, apiErr := doUnicast(ctx, sto, &Unicast{
UserId: "user1",
DeviceId: "DEV1",
AppId: "app1",
ExpireOn: future,
Data: payload,
})
c.Check(apiErr, Equals, ErrCouldNotStoreNotification)
c.Check(s.testlog.Captured(), Equals, "ERROR could not scrub channel: fail\n")
}
func (s *handlersSuite) TestDoUnicastClearPending(c *C) {
prevGenMsgId := generateMsgId
defer func() {
generateMsgId = prevGenMsgId
}()
generateMsgId = func() string {
return "MSG-ID"
}
sto := store.NewInMemoryPendingStore()
chanId := store.UnicastInternalChannelId("user1", "DEV1")
expire := store.Metadata{Expiration: time.Now().Add(4 * time.Hour)}
n := json.RawMessage("{}")
sto.AppendToUnicastChannel(chanId, "app1", n, "m1", expire)
sto.AppendToUnicastChannel(chanId, "app1", n, "m2", expire)
sto.AppendToUnicastChannel(chanId, "app1", n, "m3", expire)
sto.AppendToUnicastChannel(chanId, "app1", n, "m4", expire)
bsend := &checkBrokerSending{store: sto}
ctx := &context{testStoreAccess(nil), bsend, s.testlog}
payload := json.RawMessage(`{"a": 1}`)
res, apiErr := doUnicast(ctx, sto, &Unicast{
UserId: "user1",
DeviceId: "DEV1",
AppId: "app1",
ExpireOn: future,
Data: payload,
ClearPending: true,
})
c.Assert(apiErr, IsNil)
c.Check(res, IsNil)
c.Check(bsend.err, IsNil)
c.Check(bsend.chanId, Equals, store.UnicastInternalChannelId("user1", "DEV1"))
c.Check(bsend.top, Equals, int64(0))
c.Check(bsend.notifications, HasLen, 1)
c.Check(bsend.notifications[0], DeepEquals, protocol.Notification{
AppId: "app1",
MsgId: "MSG-ID",
Payload: payload,
})
}
func (s *handlersSuite) TestDoUnicastFromTokenFailures(c *C) {
fail := errors.New("fail")
sto := &interceptInMemoryPendingStore{
store.NewInMemoryPendingStore(),
func(meth string, err error) error {
if meth == "GetInternalChannelIdFromToken" {
return fail
}
return err
},
}
ctx := &context{logger: s.testlog}
u := &Unicast{
Token: "tok",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(`{"a": 1}`),
}
_, apiErr := doUnicast(ctx, sto, u)
c.Check(apiErr, Equals, ErrCouldNotResolveToken)
c.Check(s.testlog.Captured(), Equals, "ERROR could not resolve token: fail\n")
s.testlog.ResetCapture()
fail = store.ErrUnknownToken
_, apiErr = doUnicast(ctx, sto, u)
c.Check(apiErr, Equals, ErrUnknownToken)
c.Check(s.testlog.Captured(), Equals, "")
fail = store.ErrUnauthorized
_, apiErr = doUnicast(ctx, sto, u)
c.Check(apiErr, Equals, ErrUnauthorized)
c.Check(s.testlog.Captured(), Equals, "")
}
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 (bsend testBrokerSending) Unicast(chanIds ...store.InternalChannelId) {
// for now
if len(chanIds) != 1 {
panic("not expecting many chan ids for now")
}
bsend.chanId <- chanIds[0]
}
func (s *handlersSuite) TestRespondsToBasicSystemBroadcast(c *C) {
sto := store.NewInMemoryPendingStore()
storage := testStoreAccess(func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return sto, nil
})
bsend := testBrokerSending{make(chan store.InternalChannelId, 1)}
testServer := httptest.NewServer(MakeHandlersMux(storage, 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.Assert(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) {
storage := testStoreAccess(func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return nil, ErrStoreUnavailable
})
testServer := httptest.NewServer(MakeHandlersMux(storage, 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()
storage := testStoreAccess(func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return sto, nil
})
testServer := httptest.NewServer(MakeHandlersMux(storage, 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) {
storage := testStoreAccess(func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return store.NewInMemoryPendingStore(), nil
})
ctx := &context{storage, nil, nil}
testServer := httptest.NewServer(&JSONPostHandler{
context: ctx,
parsingBodyObj: func() interface{} { return &Broadcast{} },
doHandle: doBroadcast,
})
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) {
storage := testStoreAccess(func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return store.NewInMemoryPendingStore(), nil
})
ctx := &context{storage, nil, nil}
testServer := httptest.NewServer(&JSONPostHandler{
context: ctx,
parsingBodyObj: func() interface{} { return &Broadcast{} },
})
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(&JSONPostHandler{})
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(&JSONPostHandler{})
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(&JSONPostHandler{})
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(&JSONPostHandler{})
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) TestContentTypeWithCharset(c *C) {
testServer := httptest.NewServer(&JSONPostHandler{})
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", "application/json; charset=UTF-8")
result := checkRequestAsPost(request, 1024)
c.Assert(result, IsNil)
}
func (s *handlersSuite) TestCannotBroadcastNonPostMessages(c *C) {
testServer := httptest.NewServer(&JSONPostHandler{})
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)
}
const OK = `.*"ok":true.*`
func (s *handlersSuite) TestRespondsUnicast(c *C) {
sto := store.NewInMemoryPendingStore()
storage := testStoreAccess(func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return sto, nil
})
bsend := testBrokerSending{make(chan store.InternalChannelId, 1)}
testServer := httptest.NewServer(MakeHandlersMux(storage, bsend, s.testlog))
defer testServer.Close()
payload := json.RawMessage(`{"foo":"bar"}`)
request := newPostRequest("/notify", &Unicast{
UserId: "user2",
DeviceId: "dev3",
AppId: "app2",
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)
c.Assert(string(body), Matches, OK)
chanId := store.UnicastInternalChannelId("user2", "dev3")
c.Check(<-bsend.chanId, Equals, chanId)
top, notifications, err := sto.GetChannelSnapshot(chanId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(0))
c.Check(notifications, HasLen, 1)
}
func (s *handlersSuite) TestCheckRegister(c *C) {
registration := func() *Registration {
return &Registration{
DeviceId: "DEV1",
AppId: "app1",
}
}
reg := registration()
apiErr := checkRegister(reg)
c.Assert(apiErr, IsNil)
reg = registration()
reg.AppId = ""
apiErr = checkRegister(reg)
c.Check(apiErr, Equals, ErrMissingIdField)
reg = registration()
reg.DeviceId = ""
apiErr = checkRegister(reg)
c.Check(apiErr, Equals, ErrMissingIdField)
}
func (s *handlersSuite) TestDoRegisterMissingIdField(c *C) {
sto := store.NewInMemoryPendingStore()
token, apiErr := doRegister(nil, sto, &Registration{})
c.Check(apiErr, Equals, ErrMissingIdField)
c.Check(token, IsNil)
}
func (s *handlersSuite) TestDoRegisterCouldNotMakeToken(c *C) {
sto := &interceptInMemoryPendingStore{
store.NewInMemoryPendingStore(),
func(meth string, err error) error {
if meth == "Register" {
return errors.New("fail")
}
return err
},
}
ctx := &context{logger: s.testlog}
_, apiErr := doRegister(ctx, sto, &Registration{
DeviceId: "DEV1",
AppId: "app1",
})
c.Check(apiErr, Equals, ErrCouldNotMakeToken)
c.Check(s.testlog.Captured(), Equals, "ERROR could not make a token: fail\n")
}
func (s *handlersSuite) TestRespondsToRegisterAndUnicast(c *C) {
sto := store.NewInMemoryPendingStore()
storage := testStoreAccess(func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return sto, nil
})
bsend := testBrokerSending{make(chan store.InternalChannelId, 1)}
testServer := httptest.NewServer(MakeHandlersMux(storage, bsend, s.testlog))
defer testServer.Close()
request := newPostRequest("/register", &Registration{
DeviceId: "dev3",
AppId: "app2",
}, 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)
c.Assert(string(body), Matches, OK)
var reg map[string]interface{}
err = json.Unmarshal(body, ®)
c.Assert(err, IsNil)
token, ok := reg["token"].(string)
c.Assert(ok, Equals, true)
c.Check(token, Not(Equals), nil)
payload := json.RawMessage(`{"foo":"bar"}`)
request = newPostRequest("/notify", &Unicast{
Token: token,
AppId: "app2",
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)
c.Assert(string(body), Matches, OK)
chanId := store.UnicastInternalChannelId("dev3", "dev3")
c.Check(<-bsend.chanId, Equals, chanId)
top, notifications, err := sto.GetChannelSnapshot(chanId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(0))
c.Check(notifications, HasLen, 1)
}
func (s *handlersSuite) TestRespondsToUnregister(c *C) {
yay := make(chan bool, 1)
sto := &interceptInMemoryPendingStore{
store.NewInMemoryPendingStore(),
func(meth string, err error) error {
if meth == "Unregister" {
yay <- true
}
return err
},
}
storage := testStoreAccess(func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return sto, nil
})
bsend := testBrokerSending{make(chan store.InternalChannelId, 1)}
testServer := httptest.NewServer(MakeHandlersMux(storage, bsend, nil))
defer testServer.Close()
request := newPostRequest("/unregister", &Registration{
DeviceId: "dev3",
AppId: "app2",
}, 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)
c.Assert(string(body), Matches, OK)
c.Check(yay, HasLen, 1)
}
func (s *handlersSuite) TestDoUnregisterMissingIdField(c *C) {
sto := store.NewInMemoryPendingStore()
token, apiErr := doUnregister(nil, sto, &Registration{})
c.Check(apiErr, Equals, ErrMissingIdField)
c.Check(token, IsNil)
}
func (s *handlersSuite) TestDoUnregisterCouldNotRemoveToken(c *C) {
sto := &interceptInMemoryPendingStore{
store.NewInMemoryPendingStore(),
func(meth string, err error) error {
if meth == "Unregister" {
return errors.New("fail")
}
return err
},
}
ctx := &context{logger: s.testlog}
_, apiErr := doUnregister(ctx, sto, &Registration{
DeviceId: "DEV1",
AppId: "app1",
})
c.Check(apiErr, Equals, ErrCouldNotRemoveToken)
c.Check(s.testlog.Captured(), Equals, "ERROR could not remove token: fail\n")
}
ubuntu-push-0.68+16.04.20160310.2/server/api/middleware.go 0000644 0000156 0000165 00000002366 12670364255 023207 0 ustar pbuser pbgroup 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.68+16.04.20160310.2/server/api/handlers.go 0000644 0000156 0000165 00000037066 12670364255 022677 0 ustar pbuser pbgroup 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/base64"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"time"
"code.google.com/p/go-uuid/uuid"
"launchpad.net/ubuntu-push/logger"
"launchpad.net/ubuntu-push/protocol"
"launchpad.net/ubuntu-push/server/broker"
"launchpad.net/ubuntu-push/server/store"
)
const MaxRequestBodyBytes = 4 * 1024
const JSONMediaType = "application/json"
const MaxUnicastPayload = 2 * 1024
// 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"`
// extra information
Extra json.RawMessage `json:"extra,omitempty"`
}
// machine readable error labels
const (
ioError = "io-error"
invalidRequest = "invalid-request"
unknownChannel = "unknown-channel"
unknownToken = "unknown-token"
unauthorized = "unauthorized"
unavailable = "unavailable"
internalError = "internal"
tooManyPending = "too-many-pending"
)
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",
nil,
}
ErrRequestBodyEmpty = &APIError{
http.StatusBadRequest,
invalidRequest,
"Request body empty",
nil,
}
ErrRequestBodyTooLarge = &APIError{
http.StatusRequestEntityTooLarge,
invalidRequest,
"Request body too large",
nil,
}
ErrWrongContentType = &APIError{
http.StatusUnsupportedMediaType,
invalidRequest,
"Wrong content type, should be application/json",
nil,
}
ErrWrongRequestMethod = &APIError{
http.StatusMethodNotAllowed,
invalidRequest,
"Wrong request method, should be POST",
nil,
}
ErrWrongRequestMethodGET = &APIError{
http.StatusMethodNotAllowed,
invalidRequest,
"Wrong request method, should be GET",
nil,
}
ErrMalformedJSONObject = &APIError{
http.StatusBadRequest,
invalidRequest,
"Malformed JSON Object",
nil,
}
ErrCouldNotReadBody = &APIError{
http.StatusBadRequest,
ioError,
"Could not read request body",
nil,
}
ErrMissingIdField = &APIError{
http.StatusBadRequest,
invalidRequest,
"Missing id field",
nil,
}
ErrMissingData = &APIError{
http.StatusBadRequest,
invalidRequest,
"Missing data field",
nil,
}
ErrDataTooLarge = &APIError{
http.StatusBadRequest,
invalidRequest,
"Data too large",
nil,
}
ErrInvalidExpiration = &APIError{
http.StatusBadRequest,
invalidRequest,
"Invalid expiration date",
nil,
}
ErrPastExpiration = &APIError{
http.StatusBadRequest,
invalidRequest,
"Past expiration date",
nil,
}
ErrUnknownChannel = &APIError{
http.StatusBadRequest,
unknownChannel,
"Unknown channel",
nil,
}
ErrUnknownToken = &APIError{
http.StatusBadRequest,
unknownToken,
"Unknown token",
nil,
}
ErrUnknown = &APIError{
http.StatusInternalServerError,
internalError,
"Unknown error",
nil,
}
ErrStoreUnavailable = &APIError{
http.StatusServiceUnavailable,
unavailable,
"Message store unavailable",
nil,
}
ErrCouldNotStoreNotification = &APIError{
http.StatusServiceUnavailable,
unavailable,
"Could not store notification",
nil,
}
ErrCouldNotMakeToken = &APIError{
http.StatusServiceUnavailable,
unavailable,
"Could not make token",
nil,
}
ErrCouldNotRemoveToken = &APIError{
http.StatusServiceUnavailable,
unavailable,
"Could not remove token",
nil,
}
ErrCouldNotResolveToken = &APIError{
http.StatusServiceUnavailable,
unavailable,
"Could not resolve token",
nil,
}
ErrUnauthorized = &APIError{
http.StatusUnauthorized,
unauthorized,
"Unauthorized",
nil,
}
ErrTooManyPendingNotifications = &APIError{
http.StatusRequestEntityTooLarge,
tooManyPending,
"Too many pending notifications for this application",
nil,
}
)
func apiErrorWithExtra(apiErr *APIError, extra interface{}) *APIError {
var clone APIError = *apiErr
b, err := json.Marshal(extra)
if err != nil {
panic(fmt.Errorf("couldn't marshal our own errors: %v", err))
}
clone.Extra = json.RawMessage(b)
return &clone
}
type Registration struct {
DeviceId string `json:"deviceid"`
AppId string `json:"appid"`
}
type Unicast struct {
Token string `json:"token"`
UserId string `json:"userid"` // not part of the official API
DeviceId string `json:"deviceid"` // not part of the official API
AppId string `json:"appid"`
ExpireOn string `json:"expire_on"`
Data json.RawMessage `json:"data"`
// clear all pending messages for appid
ClearPending bool `json:"clear_pending,omitempty"`
// replace pending messages with the same replace_tag
ReplaceTag string `json:"replace_tag,omitempty"`
}
// 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 request.Method != "POST" {
return ErrWrongRequestMethod
}
if err := checkContentLength(request, maxBodySize); err != nil {
return err
}
mediaType, _, err := mime.ParseMediaType(request.Header.Get("Content-Type"))
if err != nil || mediaType != JSONMediaType {
return ErrWrongContentType
}
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 checkCastCommon(data json.RawMessage, expireOn string) (time.Time, *APIError) {
if len(data) == 0 {
return zeroTime, ErrMissingData
}
expire, err := time.Parse(time.RFC3339, expireOn)
if err != nil {
return zeroTime, ErrInvalidExpiration
}
if expire.Before(time.Now()) {
return zeroTime, ErrPastExpiration
}
return expire, nil
}
func checkBroadcast(bcast *Broadcast) (time.Time, *APIError) {
return checkCastCommon(bcast.Data, bcast.ExpireOn)
}
// StoreAccess lets get a notification pending store and parameters
// for storage.
type StoreAccess interface {
// StoreForRequest gets a pending store for the request.
StoreForRequest(w http.ResponseWriter, request *http.Request) (store.PendingStore, error)
// GetMaxNotificationsPerApplication gets the maximum number
// of pending notifications allowed for a signle application.
GetMaxNotificationsPerApplication() int
}
// context holds the interfaces to delegate to serving requests
type context struct {
storage StoreAccess
broker broker.BrokerSending
logger logger.Logger
}
func (ctx *context) getStore(w http.ResponseWriter, request *http.Request) (store.PendingStore, *APIError) {
sto, err := ctx.storage.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
}
// JSONPostHandler is able to handle POST requests with a JSON body
// delegating for the actual details.
type JSONPostHandler struct {
*context
parsingBodyObj func() interface{}
doHandle func(ctx *context, sto store.PendingStore, parsedBodyObj interface{}) (map[string]interface{}, *APIError)
}
func (h *JSONPostHandler) prepare(w http.ResponseWriter, request *http.Request) (interface{}, store.PendingStore, *APIError) {
body, apiErr := ReadBody(request, MaxRequestBodyBytes)
if apiErr != nil {
return nil, nil, apiErr
}
parsedBodyObj := h.parsingBodyObj()
err := json.Unmarshal(body, parsedBodyObj)
if err != nil {
return nil, nil, ErrMalformedJSONObject
}
sto, apiErr := h.getStore(w, request)
if apiErr != nil {
return nil, nil, apiErr
}
return parsedBodyObj, sto, nil
}
func (h *JSONPostHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
var apiErr *APIError
defer func() {
if apiErr != nil {
RespondError(writer, apiErr)
}
}()
parsedBodyObj, sto, apiErr := h.prepare(writer, request)
if apiErr != nil {
return
}
defer sto.Close()
res, apiErr := h.doHandle(h.context, sto, parsedBodyObj)
if apiErr != nil {
return
}
writer.Header().Set("Content-Type", "application/json")
if res == nil {
fmt.Fprintf(writer, `{"ok":true}`)
} else {
res["ok"] = true
resp, err := json.Marshal(res)
if err != nil {
panic(fmt.Errorf("couldn't marshal our own response: %v", err))
}
writer.Write(resp)
}
}
func doBroadcast(ctx *context, sto store.PendingStore, parsedBodyObj interface{}) (map[string]interface{}, *APIError) {
bcast := parsedBodyObj.(*Broadcast)
expire, apiErr := checkBroadcast(bcast)
if apiErr != nil {
return nil, apiErr
}
chanId, err := sto.GetInternalChannelId(bcast.Channel)
if err != nil {
switch err {
case store.ErrUnknownChannel:
return nil, ErrUnknownChannel
default:
return nil, ErrUnknown
}
}
err = sto.AppendToChannel(chanId, bcast.Data, expire)
if err != nil {
ctx.logger.Errorf("could not store notification: %v", err)
return nil, ErrCouldNotStoreNotification
}
ctx.broker.Broadcast(chanId)
return nil, nil
}
func checkUnicast(ucast *Unicast) (time.Time, *APIError) {
if ucast.AppId == "" {
return zeroTime, ErrMissingIdField
}
if ucast.Token == "" && (ucast.UserId == "" || ucast.DeviceId == "") {
return zeroTime, ErrMissingIdField
}
if len(ucast.Data) > MaxUnicastPayload {
return zeroTime, ErrDataTooLarge
}
return checkCastCommon(ucast.Data, ucast.ExpireOn)
}
// use a base64 encoded TimeUUID
var generateMsgId = func() string {
return base64.StdEncoding.EncodeToString(uuid.NewUUID())
}
func doUnicast(ctx *context, sto store.PendingStore, parsedBodyObj interface{}) (map[string]interface{}, *APIError) {
ucast := parsedBodyObj.(*Unicast)
expire, apiErr := checkUnicast(ucast)
if apiErr != nil {
return nil, apiErr
}
chanId, err := sto.GetInternalChannelIdFromToken(ucast.Token, ucast.AppId, ucast.UserId, ucast.DeviceId)
if err != nil {
switch err {
case store.ErrUnknownToken:
ctx.logger.Debugf("notify: %v %v unknown", ucast.AppId, ucast.Token)
return nil, ErrUnknownToken
case store.ErrUnauthorized:
ctx.logger.Debugf("notify: %v %v unauthorized", ucast.AppId, ucast.Token)
return nil, ErrUnauthorized
default:
ctx.logger.Errorf("could not resolve token: %v", err)
return nil, ErrCouldNotResolveToken
}
}
ctx.logger.Debugf("notify: %v %v -> %v", ucast.AppId, ucast.Token, chanId)
_, notifs, meta, err := sto.GetChannelUnfiltered(chanId)
if err != nil {
ctx.logger.Errorf("could not peek at notifications: %v", err)
return nil, ErrCouldNotStoreNotification
}
expired := 0
replaceable := 0
forApp := 0
replaceTag := ucast.ReplaceTag
scrubCriteria := []string(nil)
now := time.Now()
var last *protocol.Notification
for i, notif := range notifs {
if meta[i].Before(now) {
expired++
continue
}
if notif.AppId == ucast.AppId {
if replaceTag != "" && replaceTag == meta[i].ReplaceTag {
// this we will scrub
replaceable++
continue
}
forApp++
}
last = ¬if
}
if ucast.ClearPending {
scrubCriteria = []string{ucast.AppId}
} else if forApp >= ctx.storage.GetMaxNotificationsPerApplication() {
ctx.logger.Debugf("notify: %v %v too many pending", ucast.AppId, chanId)
return nil, apiErrorWithExtra(ErrTooManyPendingNotifications,
&last.Payload)
} else if replaceable > 0 {
scrubCriteria = []string{ucast.AppId, replaceTag}
}
if expired > 0 || scrubCriteria != nil {
err := sto.Scrub(chanId, scrubCriteria...)
if err != nil {
ctx.logger.Errorf("could not scrub channel: %v", err)
return nil, ErrCouldNotStoreNotification
}
}
msgId := generateMsgId()
meta1 := store.Metadata{
Expiration: expire,
ReplaceTag: ucast.ReplaceTag,
}
err = sto.AppendToUnicastChannel(chanId, ucast.AppId, ucast.Data, msgId, meta1)
if err != nil {
ctx.logger.Errorf("could not store notification: %v", err)
return nil, ErrCouldNotStoreNotification
}
ctx.broker.Unicast(chanId)
ctx.logger.Debugf("notify: ok %v %v id:%v clear:%v replace:%v expired:%v", ucast.AppId, chanId, msgId, ucast.ClearPending, replaceable, expired)
return nil, nil
}
func checkRegister(reg *Registration) *APIError {
if reg.DeviceId == "" || reg.AppId == "" {
return ErrMissingIdField
}
return nil
}
func doRegister(ctx *context, sto store.PendingStore, parsedBodyObj interface{}) (map[string]interface{}, *APIError) {
reg := parsedBodyObj.(*Registration)
apiErr := checkRegister(reg)
if apiErr != nil {
return nil, apiErr
}
token, err := sto.Register(reg.DeviceId, reg.AppId)
if err != nil {
ctx.logger.Errorf("could not make a token: %v", err)
return nil, ErrCouldNotMakeToken
}
return map[string]interface{}{"token": token}, nil
}
func doUnregister(ctx *context, sto store.PendingStore, parsedBodyObj interface{}) (map[string]interface{}, *APIError) {
reg := parsedBodyObj.(*Registration)
apiErr := checkRegister(reg)
if apiErr != nil {
return nil, apiErr
}
err := sto.Unregister(reg.DeviceId, reg.AppId)
if err != nil {
ctx.logger.Errorf("could not remove token: %v", err)
return nil, ErrCouldNotRemoveToken
}
return nil, nil
}
// MakeHandlersMux makes a handler that dispatches for the various API endpoints.
func MakeHandlersMux(storage StoreAccess, broker broker.BrokerSending, logger logger.Logger) *http.ServeMux {
ctx := &context{
storage: storage,
broker: broker,
logger: logger,
}
mux := http.NewServeMux()
mux.Handle("/broadcast", &JSONPostHandler{
context: ctx,
parsingBodyObj: func() interface{} { return &Broadcast{} },
doHandle: doBroadcast,
})
mux.Handle("/notify", &JSONPostHandler{
context: ctx,
parsingBodyObj: func() interface{} { return &Unicast{} },
doHandle: doUnicast,
})
mux.Handle("/register", &JSONPostHandler{
context: ctx,
parsingBodyObj: func() interface{} { return &Registration{} },
doHandle: doRegister,
})
mux.Handle("/unregister", &JSONPostHandler{
context: ctx,
parsingBodyObj: func() interface{} { return &Registration{} },
doHandle: doUnregister,
})
return mux
}
ubuntu-push-0.68+16.04.20160310.2/server/api/middleware_test.go 0000644 0000156 0000165 00000002616 12670364255 024244 0 ustar pbuser pbgroup 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.68+16.04.20160310.2/server/runner_test.go 0000644 0000156 0000165 00000012155 12670364255 022666 0 ustar pbuser pbgroup 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 (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/config"
"launchpad.net/ubuntu-push/server/listener"
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, nil)
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.*")
}
func cert() tls.Certificate {
cert, err := tls.X509KeyPair(helpers.TestCertPEMBlock, helpers.TestKeyPEMBlock)
if err != nil {
panic(err)
}
return cert
}
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",
TLSParsedConfig: TLSParsedConfig{
ParsedKeyPEMFile: "",
ParsedCertPEMFile: "",
cert: cert(),
},
}
var resource = &listener.NopSessionResourceManager{}
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, resource, &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, resource, &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, nil)
c.Assert(s.lst, Equals, lst0)
c.Check(s.kind, Equals, "http")
}
func (s *runnerSuite) TestHTTPServeRunnerTLS(c *C) {
errCh := make(chan interface{}, 1)
h := http.HandlerFunc(testHandle)
runner := HTTPServeRunner(nil, h, &testHTTPServeParsedConfig, helpers.TestTLSServerConfig)
c.Assert(s.lst, Not(IsNil))
defer s.lst.Close()
c.Check(s.kind, Equals, "http")
go func() {
defer func() {
errCh <- recover()
}()
runner()
}()
cli := http.Client{
Transport: &http.Transport{
TLSClientConfig: helpers.TestTLSClientConfig,
},
}
resp, err := cli.Get(fmt.Sprintf("https://%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.*")
}
ubuntu-push-0.68+16.04.20160310.2/server/config_test.go 0000644 0000156 0000165 00000004275 12670364255 022626 0 ustar pbuser pbgroup 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"
helpers "launchpad.net/ubuntu-push/testing"
)
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) TestTLSParsedConfigLoadPEMs(c *C) {
tmpDir := c.MkDir()
cfg := &TLSParsedConfig{
ParsedKeyPEMFile: "key.key",
ParsedCertPEMFile: "cert.cert",
}
err := cfg.LoadPEMs(tmpDir)
c.Check(err, ErrorMatches, "reading key_pem_file:.*no such file.*")
err = ioutil.WriteFile(filepath.Join(tmpDir, "key.key"), helpers.TestKeyPEMBlock, os.ModePerm)
c.Assert(err, IsNil)
err = cfg.LoadPEMs(tmpDir)
c.Check(err, ErrorMatches, "reading cert_pem_file:.*no such file.*")
err = ioutil.WriteFile(filepath.Join(tmpDir, "cert.cert"), helpers.TestCertPEMBlock, os.ModePerm)
c.Assert(err, IsNil)
err = cfg.LoadPEMs(tmpDir)
c.Assert(err, IsNil)
tlsCfg := cfg.TLSServerConfig()
c.Check(tlsCfg.Certificates, HasLen, 1)
}
ubuntu-push-0.68+16.04.20160310.2/server/tlsconfig.go 0000644 0000156 0000165 00000003775 12670364255 022316 0 ustar pbuser pbgroup 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 (
"crypto/tls"
"fmt"
"launchpad.net/ubuntu-push/config"
)
// A TLSParsedConfig holds and can be used to parse a tls server config.
type TLSParsedConfig struct {
ParsedKeyPEMFile string `json:"key_pem_file"`
ParsedCertPEMFile string `json:"cert_pem_file"`
// private post-processed config
cert tls.Certificate
}
func (cfg *TLSParsedConfig) LoadPEMs(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.cert, err = tls.X509KeyPair(certPEMBlock, keyPEMBlock)
return err
}
func (cfg *TLSParsedConfig) TLSServerConfig() *tls.Config {
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cfg.cert},
SessionTicketsDisabled: true,
// order from crypto/tls/cipher_suites.go, no RC4
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
MinVersion: tls.VersionTLS10,
}
return tlsCfg
}
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/ 0000755 0000156 0000165 00000000000 12670364532 022047 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/acceptance/ssl/ 0000755 0000156 0000165 00000000000 12670364532 022650 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/acceptance/ssl/testing.key 0000644 0000156 0000165 00000003250 12670364255 025041 0 ustar pbuser pbgroup 0000000 0000000 -----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDI+TzK6s78tW7M
0T8FnLuRq0m3tXh7Fuv6mpRyLhsvM9Q0Q0n5KzJnO+PSLBe0cbklQzYXfNJ7lfMY
T4+pW56vak3W6OCnexUZWP9nMthtCjXGiNO3zAeAmKmlYFzNqr4gneVNCIB+MbIH
XEyenP3uv9+k0uUtunJg31bJXrRVU0TGow9CAF5d+OlltJNyS26wLTnbZELieExq
fN1U/o9pLDVtRZ2m2/EHNqv6R8vitvc+pt8BAuft8amW9nzqTxuwN4ZP1drSg4Bj
1EYzFjpd1fMaa13YNUavKcAKBtqWThVZOt77tG2YUEiL6eZ6FZGs27cYhdRYbH0r
f4VEVKTVAgMBAAECggEAZWViJ5qqTdOYEFwt6L334HnEGpzDKY8aBfkBlk3uxzTm
BmxAoScLKgyMV9iJKTALUmKDovwGEfZIjOZvO+oOuL/wf9JErhsqPPyq9z0u9myl
TwJvlxaoXlgnl1lz2QwhGsGvE9uLQKAACzilK41XjKJfyn/gwt6DoJ5t4fEXGMii
Sw300qn41pb/5ZVCrELDrP1L97El6M2cjaE/bsspYUFGIPEY6NdVNovXmSW0L88X
0r85WjsIq94sTyymx01QQrSz7HsihyKhYkh/BLd9rrGNPf/ztlTsXw71ebYCsowU
coL46AsAkejosCawBDKEa1NCc0ojdNPXVb9eEoK7TQKBgQDm3BpbDsrLpmgTtuJQ
2RzMgAqEVHHTq/GC89pPljfPLBsSRZU7BmSEXFTA4mulb81vUXmkBG+v7L2tzVSA
PGaqz8qPfCgf09uN4mFDq51xaIpYrjYU5+YTsPpjYv0qwdXotDOcdkNKnx5CiZ7q
A8KwJ4CrQ0EW9D9PA2DQt9XqOwKBgQDe2/gAM58HBpHYXW7K4er9RUmvLbNIils7
ztZLkEGBENCCWGLwnx4R48HXO/XITlPb8oqv8EztTL0HceOoZZAerxl9QWUJpZ46
Ba5uDY6lb1ntUMSRDmgX00JNooskolT0YGemCIMx2NwYMfmY+WWuqa1pK5k/z6FC
fcKWph2sLwKBgEOrPKZ4PYVYL6Wns8rS+RgQaATF49+RxOcHp3Qwqgc1/HFsqAN3
KjuJ/OXU+Iyzqtn4Xdlv23ULxcWOLDiye72RzuQkFnbN2MtMEgqN4UZ+yB6aYgva
tZwMAjjjqSXBT3w4ZfB00eCrp2kFgelCVOzhh1usCQY7bdsxOE21tSRFAoGAFLPK
bfpdo4FwuvCzAhXKhoyRM7zDEtIHd57XOV3FOAAf3nvndQLTAEZwE1Z2lozwLVZy
m7Vu7/xY8wAZbeNBaBhL/d69TBAeirVMZtzLi4K0j98Y44C7Grt9RUj8NAMAcVMj
TcEsrsy+ZWD/Fr7UO0131nU+Xzcie9LC6Mu1pfECgYEAh6G7eWd/TX26u9ZB0ZsS
caFD+sut9nAWCTAR7KzZrumK5BiqcQwrKafQaKrl7RsEe0JlG9dEpJOnCG/CPkFj
odfah1tuPYhk09QQRpSvTWlMQpyICd1ezsjDyZDQLsUSl7OxOoCmrzKieS8PdFYk
OZ0Jl7ocnv+viqxetkictDM=
-----END PRIVATE KEY-----
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/ssl/README 0000644 0000156 0000165 00000000340 12670364255 023527 0 ustar pbuser pbgroup 0000000 0000000 testing.key/testing.cert
------------------------
Generated with:
openssl req -x509 -nodes -newkey rsa:2048 -multivalue-rdn -sha384 -days 3650 -keyout testing.key -out testing.cert -subj "/O=Acme Co/CN=push-delivery/"
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/ssl/testing.cert 0000644 0000156 0000165 00000002203 12670364255 025203 0 ustar pbuser pbgroup 0000000 0000000 -----BEGIN CERTIFICATE-----
MIIDJzCCAg+gAwIBAgIJAOFQ2INogVqRMA0GCSqGSIb3DQEBDAUAMCoxEDAOBgNV
BAoMB0FjbWUgQ28xFjAUBgNVBAMMDXB1c2gtZGVsaXZlcnkwHhcNMTUwNDE1MTcx
NjMyWhcNMjUwNDEyMTcxNjMyWjAqMRAwDgYDVQQKDAdBY21lIENvMRYwFAYDVQQD
DA1wdXNoLWRlbGl2ZXJ5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
yPk8yurO/LVuzNE/BZy7katJt7V4exbr+pqUci4bLzPUNENJ+SsyZzvj0iwXtHG5
JUM2F3zSe5XzGE+PqVuer2pN1ujgp3sVGVj/ZzLYbQo1xojTt8wHgJippWBczaq+
IJ3lTQiAfjGyB1xMnpz97r/fpNLlLbpyYN9WyV60VVNExqMPQgBeXfjpZbSTcktu
sC0522RC4nhManzdVP6PaSw1bUWdptvxBzar+kfL4rb3PqbfAQLn7fGplvZ86k8b
sDeGT9Xa0oOAY9RGMxY6XdXzGmtd2DVGrynACgbalk4VWTre+7RtmFBIi+nmehWR
rNu3GIXUWGx9K3+FRFSk1QIDAQABo1AwTjAdBgNVHQ4EFgQUjN2dS9quo9qDce5j
RUpgh40OJhgwHwYDVR0jBBgwFoAUjN2dS9quo9qDce5jRUpgh40OJhgwDAYDVR0T
BAUwAwEB/zANBgkqhkiG9w0BAQwFAAOCAQEAkyL9bA8KmEIdCso3uznPN6B+g9be
kaxfWeskOHWDxq4h8sa2rcAIqP4uv60DKvE1QcJhXNJHKW35TXFB9a0bosc3eNVL
Z28gEcie7yk04r2+h535CRtsgZUZ20Pr6qeNfiyrZeqGY3RHqkHfIztx0AxtYNXO
N/dTikTVLKquHpMQs07rOgkRhr1lRVl1kAZ0fj7IdWpjvMsAo8RzfRJFom9TFFa+
v3X6SAKwihDESPWRPzPtH6K/d8sRsiV3av2DlA20bUzhpi2S7FUXWAmzzzXk6wO3
GoklmlJs5nuGebuDaQ2zKvHgNIaMJinMREdtDcVGf96HKdjgILvxjrdtyA==
-----END CERTIFICATE-----
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/suites/ 0000755 0000156 0000165 00000000000 12670364532 023363 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/acceptance/suites/pingpong.go 0000644 0000156 0000165 00000006440 12670364255 025541 0 ustar pbuser pbgroup 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.68+16.04.20160310.2/server/acceptance/suites/unicast.go 0000644 0000156 0000165 00000015741 12670364255 025372 0 ustar pbuser pbgroup 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"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/server/acceptance/kit"
"launchpad.net/ubuntu-push/server/api"
)
// UnicastAcceptanceSuite has tests about unicast.
type UnicastAcceptanceSuite struct {
AcceptanceSuite
AssociatedAuth func(string) (string, string)
}
func (s *UnicastAcceptanceSuite) associatedAuth(deviceId string) (userId string, auth string) {
if s.AssociatedAuth != nil {
return s.AssociatedAuth(deviceId)
}
return deviceId, ""
}
func (s *UnicastAcceptanceSuite) TestUnicastToConnected(c *C) {
_, auth := s.associatedAuth("DEV1")
res, err := s.PostRequest("/register", &api.Registration{
DeviceId: "DEV1",
AppId: "app1",
})
c.Assert(err, IsNil, Commentf("%v", res))
events, errCh, stop := s.StartClientAuth(c, "DEV1", nil, auth, "")
got, err := s.PostRequest("/notify", &api.Unicast{
Token: res["token"].(string),
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(`{"a": 42}`),
})
c.Assert(err, IsNil, Commentf("%v", got))
c.Check(NextEvent(events, errCh), Equals, `unicast app:app1 payload:{"a":42};`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *UnicastAcceptanceSuite) TestUnicastCorrectDistribution(c *C) {
userId1, auth1 := s.associatedAuth("DEV1")
userId2, auth2 := s.associatedAuth("DEV2")
// start 1st client
events1, errCh1, stop1 := s.StartClientAuth(c, "DEV1", nil, auth1, "")
// start 2nd client
events2, errCh2, stop2 := s.StartClientAuth(c, "DEV2", nil, auth2, "")
// unicast to one and the other
got, err := s.PostRequest("/notify", &api.Unicast{
UserId: userId1,
DeviceId: "DEV1",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(`{"to": 1}`),
})
c.Assert(err, IsNil, Commentf("%v", got))
got, err = s.PostRequest("/notify", &api.Unicast{
UserId: userId2,
DeviceId: "DEV2",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(`{"to": 2}`),
})
c.Assert(err, IsNil, Commentf("%v", got))
c.Check(NextEvent(events1, errCh1), Equals, `unicast app:app1 payload:{"to":1};`)
c.Check(NextEvent(events2, errCh2), Equals, `unicast app:app1 payload:{"to":2};`)
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 *UnicastAcceptanceSuite) TestUnicastPending(c *C) {
// send unicast that will be pending
userId, auth := s.associatedAuth("DEV1")
got, err := s.PostRequest("/notify", &api.Unicast{
UserId: userId,
DeviceId: "DEV1",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(`{"a": 42}`),
})
c.Assert(err, IsNil, Commentf("%v", got))
// get pending on connect
events, errCh, stop := s.StartClientAuth(c, "DEV1", nil, auth, "")
c.Check(NextEvent(events, errCh), Equals, `unicast app:app1 payload:{"a":42};`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *UnicastAcceptanceSuite) TestUnicastLargeNeedsSplitting(c *C) {
userId, auth := s.associatedAuth("DEV2")
// send bunch of unicasts that will be pending
payloadFmt := fmt.Sprintf(`{"serial":%%d,"bloat":"%s"}`, strings.Repeat("x", 2024))
for i := 0; i < 32; i++ {
got, err := s.PostRequest("/notify", &api.Unicast{
UserId: userId,
DeviceId: "DEV2",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(fmt.Sprintf(payloadFmt, i)),
})
c.Assert(err, IsNil, Commentf("%v", got))
}
events, errCh, stop := s.StartClientAuth(c, "DEV2", nil, auth, "")
// getting pending on connect
n := 0
for {
evt := NextEvent(events, errCh)
c.Check(evt, Matches, "unicast app:app1 .*")
n += 1
if strings.Contains(evt, `"serial":31`) {
break
}
}
// was split
c.Check(n > 1, Equals, true)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *UnicastAcceptanceSuite) TestUnicastTooManyClearPending(c *C) {
userId, auth := s.associatedAuth("DEV2")
// send too many unicasts that will be pending
payloadFmt := `{"serial":%d}`
for i := 0; i < MaxNotificationsPerApplication; i++ {
got, err := s.PostRequest("/notify", &api.Unicast{
UserId: userId,
DeviceId: "DEV2",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(fmt.Sprintf(payloadFmt, i)),
})
c.Assert(err, IsNil, Commentf("%v", got))
}
got, err := s.PostRequest("/notify", &api.Unicast{
UserId: userId,
DeviceId: "DEV2",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(fmt.Sprintf(payloadFmt, MaxNotificationsPerApplication)),
})
c.Assert(err, Equals, kit.ErrNOk, Commentf("%v", got))
errorStr, _ := got["error"].(string)
c.Assert(errorStr, Equals, "too-many-pending")
// clear all pending
got, err = s.PostRequest("/notify", &api.Unicast{
UserId: userId,
DeviceId: "DEV2",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(fmt.Sprintf(payloadFmt, 1000)),
ClearPending: true,
})
c.Assert(err, IsNil, Commentf("%v", got))
events, errCh, stop := s.StartClientAuth(c, "DEV2", nil, auth, "")
// getting the 1 pending on connect
c.Check(NextEvent(events, errCh), Equals, `unicast app:app1 payload:{"serial":1000};`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
func (s *UnicastAcceptanceSuite) TestUnicastReplaceTag(c *C) {
userId, auth := s.associatedAuth("DEV2")
// send with replace_tag
got, err := s.PostRequest("/notify", &api.Unicast{
UserId: userId,
DeviceId: "DEV2",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(`{"m": 1}`),
ReplaceTag: "tagFoo",
})
c.Assert(err, IsNil, Commentf("%v", got))
// replace
got, err = s.PostRequest("/notify", &api.Unicast{
UserId: userId,
DeviceId: "DEV2",
AppId: "app1",
ExpireOn: future,
Data: json.RawMessage(`{"m": 2}`),
ReplaceTag: "tagFoo",
})
c.Assert(err, IsNil, Commentf("%v", got))
events, errCh, stop := s.StartClientAuth(c, "DEV2", nil, auth, "")
// getting the 1 pending on connect
c.Check(NextEvent(events, errCh), Equals, `unicast app:app1 payload:{"m":2};`)
stop()
c.Assert(NextEvent(s.ServerEvents, nil), Matches, `.* ended with:.*EOF`)
c.Check(len(errCh), Equals, 0)
}
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/suites/broadcast.go 0000644 0000156 0000165 00000021707 12670364255 025665 0 ustar pbuser pbgroup 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
}
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, Commentf("%v", got))
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, Commentf("%v", got))
got, err = s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 20}`),
})
c.Assert(err, IsNil, Commentf("%v", got))
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, Commentf("%v", got))
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) TestBroadcastLargeNeedsSplitting(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, Commentf("%v", got))
}
events, errCh, stop := s.StartClient(c, "DEVC", nil)
// gettting pending on connect
n := 0
for {
evt := NextEvent(events, errCh)
c.Check(evt, Matches, "broadcast chan:0 .*")
n += 1
if strings.Contains(evt, "topLevel:32") {
break
}
}
// was split
c.Check(n > 1, Equals, true)
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, Commentf("%v", got))
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, Commentf("%v", got))
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, Commentf("%v", got))
// 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, Commentf("%v", got))
got, err = s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 2}`),
})
c.Assert(err, IsNil, Commentf("%v", got))
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, Commentf("%v", got))
got, err = s.PostRequest("/broadcast", &api.Broadcast{
Channel: "system",
ExpireOn: future,
Data: json.RawMessage(`{"img1/m1": 2}`),
})
c.Assert(err, IsNil, Commentf("%v", got))
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, Commentf("%v", got))
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, Commentf("%v", got))
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)
host, err := gh.Get()
c.Assert(err, IsNil)
expected := &gethosts.Host{
Domain: "push-delivery",
Hosts: []string{s.ServerAddr},
}
c.Check(host, DeepEquals, expected)
}
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/suites/suite.go 0000644 0000156 0000165 00000013216 12670364255 025050 0 ustar pbuser pbgroup 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 (
"flag"
"fmt"
"net"
"net/http"
"os"
"regexp"
"runtime"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/server/acceptance"
"launchpad.net/ubuntu-push/server/acceptance/kit"
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
// last started session
LastSession *acceptance.ClientSession
}
// Start a client.
func (h *ServerHandle) StartClient(c *C, devId string, levels map[string]int64) (events <-chan string, errorCh <-chan error, stop func()) {
return h.StartClientAuth(c, devId, levels, "", "")
}
// Start a client with auth.
func (h *ServerHandle) StartClientAuth(c *C, devId string, levels map[string]int64, auth string, cookie string) (events <-chan string, errorCh <-chan error, stop func()) {
cliEvents, errCh, stop := h.StartClientAuthFlex(c, devId, levels, auth, cookie, regexp.QuoteMeta(devId))
c.Assert(NextEvent(cliEvents, errCh), Matches, "connected .*")
return cliEvents, errCh, stop
}
// Start a client with auth, take a devId regexp, don't check any client event.
func (h *ServerHandle) StartClientAuthFlex(c *C, devId string, levels map[string]int64, auth, cookie, devIdRegexp string) (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
sess.Auth = auth
if auth != "" {
sess.ExchangeTimeout = 5 * time.Second
}
if cookie != "" {
sess.SetCookie(cookie)
sess.ReportSetParams = true
}
err := sess.Dial()
c.Assert(err, IsNil)
h.LastSession = sess
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(h.ServerEvents, nil), Matches, ".*session.* connected .*")
c.Assert(NextEvent(h.ServerEvents, nil), Matches, ".*session.* registered "+devIdRegexp)
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
kit.APIClient // has ServerAPIURL
// KillGroup should be populated by StartServer with functions
// to kill the server process
KillGroup map[string]func(os.Signal)
}
// 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.SetupClient(nil, false, http.DefaultMaxIdleConnsPerHost)
}
func (s *AcceptanceSuite) TearDownTest(c *C) {
for _, f := range s.KillGroup {
f(os.Kill)
}
}
func testClientSession(addr string, deviceId, model, imageChannel string, reportPings bool) *acceptance.ClientSession {
tlsConfig, err := kit.MakeTLSConfig("push-delivery", false, 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,
DeviceId: deviceId,
Model: model,
ImageChannel: imageChannel,
ReportPings: reportPings,
TLSConfig: tlsConfig,
}
}
// 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
}
// Long after the end of the tests.
var future = time.Now().Add(9 * time.Hour).Format(time.RFC3339)
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/suites/helpers.go 0000644 0000156 0000165 00000011047 12670364255 025361 0 ustar pbuser pbgroup 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"),
})
}
const MaxNotificationsPerApplication = 45
// 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",
"max_notifications_per_app": MaxNotificationsPerApplication,
})
}
// 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("^.*?(?: .+\\.go:\\d+:)? ([[: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
}
if strings.HasPrefix(info, "DEBUG ") && !strings.HasPrefix(info, "DEBUG session(") {
// skip non session DEBUG logs
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 "
)
// ExtractListeningAddr goes over logs 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, 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.68+16.04.20160310.2/server/acceptance/kit/ 0000755 0000156 0000165 00000000000 12670364532 022636 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/acceptance/kit/cliloop.go 0000644 0000156 0000165 00000013747 12670364255 024644 0 ustar pbuser pbgroup 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 kit
import (
"crypto/tls"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"launchpad.net/ubuntu-push/external/murmur3"
"launchpad.net/ubuntu-push/config"
"launchpad.net/ubuntu-push/server/acceptance"
)
type Configuration struct {
// session configuration
ExchangeTimeout config.ConfigTimeDuration `json:"exchange_timeout"`
// server connection config
Target string `json:"target" help:"production|staging - picks defaults"`
Addr config.ConfigHostPort `json:"addr"`
Vnode string `json:"vnode" help:"vnode postfix to make up a targeting device-id"`
CertPEMFile string `json:"cert_pem_file"`
Insecure bool `json:"insecure" help:"disable checking of server certificate and hostname"`
Domain string `json:"domain" help:"domain for tls connect"`
// api config
APIURL string `json:"api" help:"api url"`
APICertPEMFile string `json:"api_cert_pem_file"`
// run timeout
RunTimeout config.ConfigTimeDuration `json:"run_timeout"`
// flags
ReportPings bool `json:"reportPings" help:"report each Ping from the server"`
DeviceModel string `json:"model" help:"device image model"`
ImageChannel string `json:"imageChannel" help:"image channel"`
BuildNumber int32 `json:"buildNumber" help:"build number"`
}
func (cfg *Configuration) PickByTarget(what, productionValue, stagingValue string) (value string) {
switch cfg.Target {
case "production":
value = productionValue
case "staging":
value = stagingValue
case "":
log.Fatalf("either %s or target must be given", what)
default:
log.Fatalf("if specified target should be production|staging")
}
return
}
// Control.
var (
Name = "acceptanceclient"
Defaults = map[string]interface{}{
"target": "",
"addr": ":0",
"vnode": "",
"exchange_timeout": "5s",
"cert_pem_file": "",
"insecure": false,
"domain": "",
"run_timeout": "0s",
"reportPings": true,
"model": "?",
"imageChannel": "?",
"buildNumber": -1,
"api": "",
"api_cert_pem_file": "",
}
)
// CliLoop parses command line arguments and runs a client loop.
func CliLoop(totalCfg interface{}, cfg *Configuration, onSetup func(sess *acceptance.ClientSession, apiCli *APIClient, cfgDir string), auth func(string) string, waitFor func() string, onConnect func()) {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] \n", Name)
flag.PrintDefaults()
}
missingArg := func(what string) {
fmt.Fprintf(os.Stderr, "missing %s\n", what)
flag.Usage()
os.Exit(2)
}
err := config.ReadFilesDefaults(totalCfg, Defaults, "")
if err != nil {
log.Fatalf("reading config: %v", err)
}
deviceId := ""
if cfg.Vnode != "" {
if cfg.Addr == ":0" {
log.Fatalf("-vnode needs -addr specified")
}
deviceId = cfg.Addr.HostPort() + "|" + cfg.Vnode
log.Printf("using device-id: %q", deviceId)
} else {
narg := flag.NArg()
switch {
case narg < 1:
missingArg("device-id")
}
deviceId = flag.Arg(0)
}
cfgDir := filepath.Dir(flag.Lookup("cfg@").Value.String())
// setup api
apiCli := &APIClient{}
var apiTLSConfig *tls.Config
if cfg.APICertPEMFile != "" || cfg.Insecure {
var err error
apiTLSConfig, err = MakeTLSConfig("", cfg.Insecure,
cfg.APICertPEMFile, cfgDir)
if err != nil {
log.Fatalf("api tls config: %v", err)
}
}
apiCli.SetupClient(apiTLSConfig, true, 1)
if cfg.APIURL == "" {
apiCli.ServerAPIURL = cfg.PickByTarget("api",
"https://push.ubuntu.com",
"https://push.staging.ubuntu.com")
} else {
apiCli.ServerAPIURL = cfg.APIURL
}
addr := ""
domain := ""
if cfg.Addr == ":0" {
hash := murmur3.Sum64([]byte(deviceId))
hosts, err := apiCli.GetRequest("/delivery-hosts",
map[string]string{
"h": fmt.Sprintf("%x", hash),
})
if err != nil {
log.Fatalf("querying hosts: %v", err)
}
addr = hosts["hosts"].([]interface{})[0].(string)
domain = hosts["domain"].(string)
log.Printf("using: %s %s", addr, domain)
} else {
addr = cfg.Addr.HostPort()
domain = cfg.Domain
}
session := &acceptance.ClientSession{
ExchangeTimeout: cfg.ExchangeTimeout.TimeDuration(),
ServerAddr: addr,
DeviceId: deviceId,
// flags
Model: cfg.DeviceModel,
ImageChannel: cfg.ImageChannel,
BuildNumber: cfg.BuildNumber,
ReportPings: cfg.ReportPings,
}
onSetup(session, apiCli, cfgDir)
session.TLSConfig, err = MakeTLSConfig(domain, cfg.Insecure, cfg.CertPEMFile, cfgDir)
if err != nil {
log.Fatalf("tls config: %v", err)
}
session.Auth = auth("https://push.ubuntu.com/")
var waitForRegexp *regexp.Regexp
waitForStr := waitFor()
if waitForStr != "" {
var err error
waitForRegexp, err = regexp.Compile(waitForStr)
if err != nil {
log.Fatalf("wait_for regexp: %v", err)
}
}
err = session.Dial()
if err != nil {
log.Fatalln(err)
}
events := make(chan string, 5)
go func() {
for {
ev := <-events
if strings.HasPrefix(ev, "connected") {
onConnect()
}
if waitForRegexp != nil && waitForRegexp.MatchString(ev) {
log.Println(":", ev)
os.Exit(0)
}
log.Println(ev)
}
}()
if cfg.RunTimeout.TimeDuration() != 0 {
time.AfterFunc(cfg.RunTimeout.TimeDuration(), func() {
log.Fatalln("")
})
}
err = session.Run(events)
if err != nil {
log.Fatalln(err)
}
}
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/kit/helpers.go 0000644 0000156 0000165 00000002576 12670364255 024643 0 ustar pbuser pbgroup 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 kit
import (
"crypto/tls"
"crypto/x509"
"fmt"
"launchpad.net/ubuntu-push/config"
)
// MakeTLSConfig makes a tls.Config, optionally reading a cert from
// disk, possibly relative to relDir.
func MakeTLSConfig(domain string, insecure bool, certPEMFile string, relDir string) (*tls.Config, error) {
tlsConfig := &tls.Config{}
tlsConfig.ServerName = domain
tlsConfig.InsecureSkipVerify = insecure
if !insecure && certPEMFile != "" {
certPEMBlock, err := config.LoadFile(certPEMFile, relDir)
if err != nil {
return nil, fmt.Errorf("reading cert: %v", err)
}
cp := x509.NewCertPool()
ok := cp.AppendCertsFromPEM(certPEMBlock)
if !ok {
return nil, fmt.Errorf("could not parse certificate")
}
tlsConfig.RootCAs = cp
}
return tlsConfig, nil
}
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/kit/api.go 0000644 0000156 0000165 00000006153 12670364255 023745 0 ustar pbuser pbgroup 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 kit contains reusable building blocks for acceptance.
package kit
import (
"bytes"
_ "crypto/sha512" // support sha384/512 certs
"crypto/tls"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"net/url"
)
// APIClient helps making api requests.
type APIClient struct {
ServerAPIURL string
// hook to adjust requests
MassageRequest func(req *http.Request, message interface{}) *http.Request
// other state
httpClient *http.Client
}
type APIError struct {
Msg string
Body []byte
}
func (e *APIError) Error() string {
return e.Msg
}
// SetupClient sets up the http client to make requests.
func (api *APIClient) SetupClient(tlsConfig *tls.Config, disableKeepAlives bool, maxIdleConnsPerHost int) {
api.httpClient = &http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig,
DisableKeepAlives: disableKeepAlives,
MaxIdleConnsPerHost: maxIdleConnsPerHost},
}
}
var ErrNOk = errors.New("not ok")
func readBody(respBody io.ReadCloser) (map[string]interface{}, error) {
defer respBody.Close()
body, err := ioutil.ReadAll(respBody)
if err != nil {
return nil, err
}
var res map[string]interface{}
err = json.Unmarshal(body, &res)
if err != nil {
return nil, &APIError{err.Error(), body}
}
return res, nil
}
// Post a API request.
func (api *APIClient) PostRequest(path string, message interface{}) (map[string]interface{}, error) {
packedMessage, err := json.Marshal(message)
if err != nil {
panic(err)
}
reader := bytes.NewReader(packedMessage)
url := api.ServerAPIURL + path
request, _ := http.NewRequest("POST", url, reader)
request.ContentLength = int64(reader.Len())
request.Header.Set("Content-Type", "application/json")
if api.MassageRequest != nil {
request = api.MassageRequest(request, message)
}
resp, err := api.httpClient.Do(request)
if err != nil {
return nil, err
}
res, err := readBody(resp.Body)
if err != nil {
return nil, err
}
if ok, _ := res["ok"].(bool); !ok {
return res, ErrNOk
}
return res, nil
}
// Get resource from API endpoint.
func (api *APIClient) GetRequest(path string, params map[string]string) (map[string]interface{}, error) {
apiURL := api.ServerAPIURL + path
if len(params) != 0 {
vals := url.Values{}
for k, v := range params {
vals.Set(k, v)
}
apiURL += "?" + vals.Encode()
}
request, _ := http.NewRequest("GET", apiURL, nil)
resp, err := api.httpClient.Do(request)
if err != nil {
return nil, err
}
res, err := readBody(resp.Body)
if err != nil {
return nil, err
}
return res, nil
}
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/cmd/ 0000755 0000156 0000165 00000000000 12670364532 022612 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/acceptance/cmd/acceptanceclient.go 0000644 0000156 0000165 00000002736 12670364255 026440 0 ustar pbuser pbgroup 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 (
"log"
"os/exec"
"strings"
"launchpad.net/ubuntu-push/server/acceptance"
"launchpad.net/ubuntu-push/server/acceptance/kit"
)
type configuration struct {
kit.Configuration
AuthHelper string `json:"auth_helper"`
WaitFor string `json:"wait_for"`
}
func main() {
kit.Defaults["auth_helper"] = ""
kit.Defaults["wait_for"] = ""
cfg := &configuration{}
kit.CliLoop(cfg, &cfg.Configuration, func(session *acceptance.ClientSession, apiCli *kit.APIClient, cfgDir string) {
log.Printf("with: %#v", session)
}, func(url string) string {
if cfg.AuthHelper == "" {
return ""
}
auth, err := exec.Command(cfg.AuthHelper, url).Output()
if err != nil {
log.Fatalf("auth helper: %v", err)
}
return strings.TrimSpace(string(auth))
}, func() string {
return cfg.WaitFor
}, func() {
})
}
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/acceptanceclient.go 0000644 0000156 0000165 00000012515 12670364255 025671 0 ustar pbuser pbgroup 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/sha512" // support sha384/512 certs
"crypto/tls"
"encoding/json"
"fmt"
"net"
"strings"
"sync"
"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
BuildNumber int32
ServerAddr string
ExchangeTimeout time.Duration
ReportPings bool
Levels map[string]int64
TLSConfig *tls.Config
Prefix string // prefix for events
Auth string
cookie string
cookieLock sync.RWMutex
ReportSetParams bool
DontClose bool
SlowStart time.Duration
// connection
Connection net.Conn
}
// GetCookie gets the current cookie.
func (sess *ClientSession) GetCookie() string {
sess.cookieLock.RLock()
defer sess.cookieLock.RUnlock()
return sess.cookie
}
// SetCookie sets the current cookie.
func (sess *ClientSession) SetCookie(cookie string) {
sess.cookieLock.Lock()
defer sess.cookieLock.Unlock()
sess.cookie = cookie
}
// 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
}
sess.TLSWrapAndSet(conn)
return nil
}
// TLSWrapAndSet wraps a socket connection in tls and sets it as
// session.Connection. For use instead of Dial().
func (sess *ClientSession) TLSWrapAndSet(conn net.Conn) {
var tlsConfig *tls.Config
if sess.TLSConfig != nil {
tlsConfig = sess.TLSConfig
} else {
tlsConfig = &tls.Config{}
}
sess.Connection = tls.Client(conn, tlsConfig)
}
type serverMsg struct {
Type string `json:"T"`
protocol.BroadcastMsg
protocol.NotificationsMsg
protocol.ConnWarnMsg
protocol.SetParamsMsg
}
// Run the session with the server, emits a stream of events.
func (sess *ClientSession) Run(events chan<- string) error {
conn := sess.Connection
if !sess.DontClose {
defer conn.Close()
}
time.Sleep(sess.SlowStart)
conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
_, err := conn.Write(wireVersionBytes)
if err != nil {
return err
}
proto := protocol.NewProtocol0(conn)
info := map[string]interface{}{
"device": sess.Model,
"channel": sess.ImageChannel,
}
if sess.BuildNumber != -1 {
info["build_number"] = sess.BuildNumber
}
err = proto.WriteMessage(protocol.ConnectMsg{
Type: "connect",
DeviceId: sess.DeviceId,
Levels: sess.Levels,
Info: info,
Authorization: sess.Auth,
Cookie: sess.GetCookie(),
})
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 "notifications":
conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
err := proto.WriteMessage(protocol.AckMsg{Type: "ack"})
if err != nil {
return err
}
parts := make([]string, len(recv.Notifications))
for i, notif := range recv.Notifications {
pack, err := json.Marshal(¬if.Payload)
if err != nil {
return err
}
parts[i] = fmt.Sprintf("app:%v payload:%s;", notif.AppId, pack)
}
events <- fmt.Sprintf("%sunicast %s", sess.Prefix, strings.Join(parts, " "))
case "broadcast":
conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout))
err := proto.WriteMessage(protocol.AckMsg{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)
case "warn", "connwarn":
events <- fmt.Sprintf("%sconnwarn %s", sess.Prefix, recv.Reason)
case "connbroken":
events <- fmt.Sprintf("%sconnbroken %s", sess.Prefix, recv.Reason)
case "setparams":
sess.SetCookie(recv.SetCookie)
if sess.ReportSetParams {
events <- sess.Prefix + "setparams"
}
}
}
return nil
}
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/acceptance_test.go 0000644 0000156 0000165 00000004167 12670364255 025535 0 ustar pbuser pbgroup 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"] = "push-delivery"
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}})
// unicast
var _ = Suite(&suites.UnicastAcceptanceSuite{suites.AcceptanceSuite{StartServer: StartServer}, nil})
ubuntu-push-0.68+16.04.20160310.2/server/acceptance/acceptance.sh 0000755 0000156 0000165 00000000553 12670364255 024501 0 ustar pbuser pbgroup 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.68+16.04.20160310.2/server/broker/ 0000755 0000156 0000165 00000000000 12670364532 021245 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/broker/broker.go 0000644 0000156 0000165 00000010137 12670364255 023064 0 ustar pbuser pbgroup 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"
)
type SessionTracker interface {
// SessionId
SessionId() string
}
// Broker is responsible for registring sessions and delivering messages
// through them.
type Broker interface {
// Register the session.
Register(connMsg *protocol.ConnectMsg, track SessionTracker) (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)
// Unicast over channels.
Unicast(chanIds ...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 retrieveng 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
}
// GetInfoInt helps retrieving an integer out of a protocol.ConnectMsg.Info.
func GetInfoInt(msg *protocol.ConnectMsg, name string, defaultVal int) (int, error) {
v, ok := msg.Info[name]
if !ok {
return defaultVal, nil
}
n, ok := v.(float64)
if !ok {
return -1, ErrUnexpectedValue
}
return int(n), 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
// Get gets the content of the channel with chanId.
Get(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error)
// DropByMsgId drops notifications from the channel chanId by message id.
DropByMsgId(chanId store.InternalChannelId, targets []protocol.Notification) error
// Feed feeds exchange into the session.
Feed(Exchange)
// InternalChannelId() returns the channel id corresponding to the session.
InternalChannelId() store.InternalChannelId
}
// 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.68+16.04.20160310.2/server/broker/simple/ 0000755 0000156 0000165 00000000000 12670364532 022536 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/broker/simple/suite_test.go 0000644 0000156 0000165 00000003503 12670364255 025260 0 ustar pbuser pbgroup 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
}
// trivial session tracker
type testTracker string
func (t testTracker) SessionId() string {
return string(t)
}
var _ = Suite(&commonBrokerSuite{testsuite.CommonBrokerSuite{
MakeBroker: func(sto store.PendingStore, cfg broker.BrokerConfig, log logger.Logger) testsuite.FullBroker {
return NewSimpleBroker(sto, cfg, log)
},
MakeTracker: func(sessionId string) broker.SessionTracker {
return testTracker(sessionId)
},
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)
},
RevealUnicastExchange: func(exchg broker.Exchange) *broker.UnicastExchange {
return exchg.(*broker.UnicastExchange)
},
}})
ubuntu-push-0.68+16.04.20160310.2/server/broker/simple/simple.go 0000644 0000156 0000165 00000017151 12670364255 024365 0 ustar pbuser pbgroup 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 {
broker *SimpleBroker
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
unicastDelivery
)
// 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
}
func (sess *simpleBrokerSession) Get(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error) {
return sess.broker.get(chanId, cachedOk)
}
func (sess *simpleBrokerSession) DropByMsgId(chanId store.InternalChannelId, targets []protocol.Notification) error {
return sess.broker.drop(chanId, targets)
}
func (sess *simpleBrokerSession) Feed(exchg broker.Exchange) {
sess.exchanges <- exchg
}
func (sess *simpleBrokerSession) InternalChannelId() store.InternalChannelId {
return store.UnicastInternalChannelId(sess.deviceId, sess.deviceId)
}
// 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
}
// Register registers a session with the broker. It feeds the session
// pending notifications as well.
func (b *SimpleBroker) Register(connect *protocol.ConnectMsg, track broker.SessionTracker) (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{
broker: b,
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 = broker.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
}
func (b *SimpleBroker) get(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error) {
topLevel, notifications, err := b.sto.GetChannelSnapshot(chanId)
if err != nil {
b.logger.Errorf("unsuccessful, get channel snapshot for %v (cachedOk=%v): %v", chanId, cachedOk, err)
}
return topLevel, notifications, err
}
func (b *SimpleBroker) drop(chanId store.InternalChannelId, targets []protocol.Notification) error {
err := b.sto.DropByMsgId(chanId, targets)
if err != nil {
b.logger.Errorf("unsuccessful, drop from channel %v: %v", chanId, err)
}
return err
}
// 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
prev.exchanges <- nil
}
b.registry[sess.deviceId] = sess
sess.registered = true
sess.done <- true
}
case delivery := <-b.deliveryCh:
switch delivery.kind {
case broadcastDelivery:
topLevel, notifications, err := b.get(delivery.chanId, false)
if err != nil {
// next broadcast will try again
continue Loop
}
broadcastExchg := &broker.BroadcastExchange{
ChanId: delivery.chanId,
TopLevel: topLevel,
Notifications: notifications,
}
broadcastExchg.Init()
for _, sess := range b.registry {
sess.exchanges <- broadcastExchg
}
case unicastDelivery:
chanId := delivery.chanId
_, devId := chanId.UnicastUserAndDevice()
sess := b.registry[devId]
if sess != nil {
sess.exchanges <- &broker.UnicastExchange{ChanId: chanId, CachedOk: false}
}
}
}
}
}
// Broadcast requests the broadcast for a channel.
func (b *SimpleBroker) Broadcast(chanId store.InternalChannelId) {
b.deliveryCh <- &delivery{
kind: broadcastDelivery,
chanId: chanId,
}
}
// Unicast requests unicast for the channels.
func (b *SimpleBroker) Unicast(chanIds ...store.InternalChannelId) {
for _, chanId := range chanIds {
b.deliveryCh <- &delivery{
kind: unicastDelivery,
chanId: chanId,
}
}
}
ubuntu-push-0.68+16.04.20160310.2/server/broker/simple/simple_test.go 0000644 0000156 0000165 00000002574 12670364255 025427 0 ustar pbuser pbgroup 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 (
stdtesting "testing"
. "launchpad.net/gocheck"
"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) TestSessionInternalChannelId(c *C) {
sess := &simpleBrokerSession{deviceId: "dev21"}
c.Check(sess.InternalChannelId(), Equals, store.UnicastInternalChannelId("dev21", "dev21"))
}
ubuntu-push-0.68+16.04.20160310.2/server/broker/exchanges.go 0000644 0000156 0000165 00000014347 12670364255 023554 0 ustar pbuser pbgroup 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"
"time"
"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
notificationsMsg protocol.NotificationsMsg
ackMsg protocol.AckMsg
}
type BaseExchange struct {
Timestamp time.Time
}
// BroadcastExchange leads a session through delivering a BROADCAST.
// For simplicity it is fully public.
type BroadcastExchange struct {
ChanId store.InternalChannelId
TopLevel int64
Notifications []protocol.Notification
Decoded []map[string]interface{}
BaseExchange
}
// 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.Notifications))
sbe.Decoded = decoded
for i, notif := range sbe.Notifications {
err := json.Unmarshal(notif.Payload, &decoded[i])
if err != nil {
decoded[i] = nil
}
}
}
func filterByLevel(clientLevel, topLevel int64, notifs []protocol.Notification) []protocol.Notification {
c := int64(len(notifs))
if c == 0 {
return nil
}
delta := topLevel - clientLevel
if delta < 0 { // means too ahead, send the last pending
delta = 1
}
if delta < c {
return notifs[c-delta:]
} else {
return notifs
}
}
func channelFilter(tag string, chanId store.InternalChannelId, notifs []protocol.Notification, decoded []map[string]interface{}) []json.RawMessage {
if len(notifs) != 0 && chanId == store.SystemInternalChannelId {
decoded := decoded[len(decoded)-len(notifs):]
filtered := make([]json.RawMessage, 0)
for i, decoded1 := range decoded {
if _, ok := decoded1[tag]; ok {
filtered = append(filtered, notifs[i].Payload)
}
}
return filtered
}
return protocol.ExtractPayloads(notifs)
}
// Prepare session for a BROADCAST.
func (sbe *BroadcastExchange) Prepare(sess BrokerSession) (outMessage protocol.SplittableMsg, inMessage interface{}, err error) {
clientLevel := sess.Levels()[sbe.ChanId]
notifs := filterByLevel(clientLevel, sbe.TopLevel, sbe.Notifications)
tag := fmt.Sprintf("%s/%s", sess.DeviceImageChannel(), sess.DeviceImageModel())
payloads := channelFilter(tag, sbe.ChanId, notifs, 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()
// 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
}
// ConnMetaExchange allows to send a CONNBROKEN or CONNWARN message.
type ConnMetaExchange struct {
Msg protocol.OnewayMsg
}
// check interface already here
var _ Exchange = (*ConnMetaExchange)(nil)
// Prepare session for a CONNBROKEN/WARN.
func (cbe *ConnMetaExchange) Prepare(sess BrokerSession) (outMessage protocol.SplittableMsg, inMessage interface{}, err error) {
return cbe.Msg, nil, nil
}
// CONNBROKEN/WARN aren't acked.
func (cbe *ConnMetaExchange) Acked(sess BrokerSession, done bool) error {
panic("Acked should not get invoked on ConnMetaExchange")
}
// UnicastExchange leads a session through delivering a NOTIFICATIONS message.
// For simplicity it is fully public.
type UnicastExchange struct {
ChanId store.InternalChannelId
CachedOk bool
BaseExchange
}
// check interface already here
var _ Exchange = (*UnicastExchange)(nil)
// Prepare session for a NOTIFICATIONS.
func (sue *UnicastExchange) Prepare(sess BrokerSession) (outMessage protocol.SplittableMsg, inMessage interface{}, err error) {
_, notifs, err := sess.Get(sue.ChanId, sue.CachedOk)
if err != nil {
return nil, nil, err
}
if len(notifs) == 0 {
return nil, nil, ErrNop
}
scratchArea := sess.ExchangeScratchArea()
scratchArea.notificationsMsg.Reset()
scratchArea.notificationsMsg.Notifications = notifs
return &scratchArea.notificationsMsg, &scratchArea.ackMsg, nil
}
// Acked deals with an ACK for a NOTIFICATIONS.
func (sue *UnicastExchange) Acked(sess BrokerSession, done bool) error {
scratchArea := sess.ExchangeScratchArea()
if scratchArea.ackMsg.Type != "ack" {
return &ErrAbort{"expected ACK message"}
}
err := sess.DropByMsgId(sue.ChanId, scratchArea.notificationsMsg.Notifications)
if err != nil {
return err
}
return nil
}
// FeedPending feeds exchanges covering pending notifications into the session.
func FeedPending(sess BrokerSession) error {
// find relevant channels, for now only system
channels := []store.InternalChannelId{store.SystemInternalChannelId}
for _, chanId := range channels {
topLevel, notifications, err := sess.Get(chanId, true)
if err != nil {
// next broadcast will try again
continue
}
clientLevel := sess.Levels()[chanId]
if clientLevel != topLevel {
broadcastExchg := &BroadcastExchange{
ChanId: chanId,
TopLevel: topLevel,
Notifications: notifications,
}
broadcastExchg.Init()
sess.Feed(broadcastExchg)
}
}
sess.Feed(&UnicastExchange{ChanId: sess.InternalChannelId(), CachedOk: true})
return nil
}
ubuntu-push-0.68+16.04.20160310.2/server/broker/testing/ 0000755 0000156 0000165 00000000000 12670364532 022722 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/broker/testing/impls.go 0000644 0000156 0000165 00000005214 12670364255 024401 0 ustar pbuser pbgroup 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/protocol"
"launchpad.net/ubuntu-push/server/broker"
"launchpad.net/ubuntu-push/server/store"
)
// Test implementation of BrokerSession.
type TestBrokerSession struct {
DeviceId string
Model string
ImageChannel string
Exchanges chan broker.Exchange
LevelsMap broker.LevelsMap
exchgScratch broker.ExchangesScratchArea
// hooks
DoGet func(store.InternalChannelId, bool) (int64, []protocol.Notification, error)
DoDropByMsgId func(store.InternalChannelId, []protocol.Notification) error
}
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
}
func (tbs *TestBrokerSession) Get(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error) {
return tbs.DoGet(chanId, cachedOk)
}
func (tbs *TestBrokerSession) DropByMsgId(chanId store.InternalChannelId, targets []protocol.Notification) error {
return tbs.DoDropByMsgId(chanId, targets)
}
func (tbs *TestBrokerSession) Feed(exchg broker.Exchange) {
tbs.Exchanges <- exchg
}
func (tbs *TestBrokerSession) InternalChannelId() store.InternalChannelId {
return store.UnicastInternalChannelId(tbs.DeviceId, tbs.DeviceId)
}
// 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.68+16.04.20160310.2/server/broker/exchanges_test.go 0000644 0000156 0000165 00000033245 12670364255 024611 0 ustar pbuser pbgroup 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"
"errors"
"fmt"
"strings"
stdtesting "testing"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/protocol"
"launchpad.net/ubuntu-push/server/broker"
"launchpad.net/ubuntu-push/server/broker/testing"
"launchpad.net/ubuntu-push/server/store"
help "launchpad.net/ubuntu-push/testing"
)
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,
Notifications: help.Ns(
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,
Notifications: help.Ns(
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,
Notifications: []protocol.Notification{},
}
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,
Notifications: []protocol.Notification{},
}
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,
Notifications: help.Ns(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,
Notifications: help.Ns(
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,
Notifications: help.Ns(
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,
Notifications: help.Ns(
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,
Notifications: help.Ns(
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) TestConnMetaExchange(c *C) {
sess := &testing.TestBrokerSession{}
var msg protocol.OnewayMsg = &protocol.ConnWarnMsg{"connwarn", "REASON"}
cbe := &broker.ConnMetaExchange{msg}
outMsg, inMsg, err := cbe.Prepare(sess)
c.Assert(err, IsNil)
c.Check(msg, Equals, outMsg)
c.Check(inMsg, IsNil) // no answer is expected
// check
marshalled, err := json.Marshal(outMsg)
c.Assert(err, IsNil)
c.Check(string(marshalled), Equals, `{"T":"connwarn","Reason":"REASON"}`)
c.Check(func() { cbe.Acked(nil, true) }, PanicMatches, "Acked should not get invoked on ConnMetaExchange")
}
func (s *exchangesSuite) TestUnicastExchange(c *C) {
chanId1 := store.UnicastInternalChannelId("u1", "d1")
notifs := []protocol.Notification{
protocol.Notification{
MsgId: "msg1",
AppId: "app1",
Payload: json.RawMessage(`{"m": 1}`),
},
protocol.Notification{
MsgId: "msg2",
AppId: "app2",
Payload: json.RawMessage(`{"m": 2}`),
},
}
dropped := make(chan []protocol.Notification, 2)
sess := &testing.TestBrokerSession{
DoGet: func(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error) {
c.Check(chanId, Equals, chanId1)
c.Check(cachedOk, Equals, false)
return 0, notifs, nil
},
DoDropByMsgId: func(chanId store.InternalChannelId, targets []protocol.Notification) error {
c.Check(chanId, Equals, chanId1)
dropped <- targets
return nil
},
}
exchg := &broker.UnicastExchange{ChanId: chanId1, CachedOk: false}
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":"notifications","Notifications":[{"A":"app1","M":"msg1","P":{"m":1}},{"A":"app2","M":"msg2","P":{"m":2}}]}`)
err = json.Unmarshal([]byte(`{"T":"ack"}`), inMsg)
c.Assert(err, IsNil)
err = exchg.Acked(sess, true)
c.Assert(err, IsNil)
c.Check(dropped, HasLen, 1)
c.Check(<-dropped, DeepEquals, notifs)
}
func (s *exchangesSuite) TestUnicastExchangeAckMismatch(c *C) {
notifs := []protocol.Notification{protocol.Notification{}}
dropped := make(chan []protocol.Notification, 2)
sess := &testing.TestBrokerSession{
DoGet: func(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error) {
return 0, notifs, nil
},
DoDropByMsgId: func(chanId store.InternalChannelId, targets []protocol.Notification) error {
dropped <- targets
return nil
},
}
exchg := &broker.UnicastExchange{}
_, inMsg, err := exchg.Prepare(sess)
c.Assert(err, IsNil)
err = json.Unmarshal([]byte(`{}`), inMsg)
c.Assert(err, IsNil)
err = exchg.Acked(sess, true)
c.Assert(err, Not(IsNil))
c.Check(dropped, HasLen, 0)
}
func (s *exchangesSuite) TestUnicastExchangeErrorOnPrepare(c *C) {
fail := errors.New("fail")
sess := &testing.TestBrokerSession{
DoGet: func(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error) {
return 0, nil, fail
},
}
exchg := &broker.UnicastExchange{}
_, _, err := exchg.Prepare(sess)
c.Assert(err, Equals, fail)
}
func (s *exchangesSuite) TestUnicastExchangeCachedOkNop(c *C) {
chanId1 := store.UnicastInternalChannelId("u1", "d1")
sess := &testing.TestBrokerSession{
DoGet: func(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error) {
c.Check(chanId, Equals, chanId1)
c.Check(cachedOk, Equals, true)
return 0, nil, nil
},
}
exchg := &broker.UnicastExchange{ChanId: chanId1, CachedOk: true}
_, _, err := exchg.Prepare(sess)
c.Assert(err, Equals, broker.ErrNop)
}
func (s *exchangesSuite) TestUnicastExchangeErrorOnAcked(c *C) {
notifs := []protocol.Notification{protocol.Notification{}}
fail := errors.New("fail")
sess := &testing.TestBrokerSession{
DoGet: func(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error) {
return 0, notifs, nil
},
DoDropByMsgId: func(chanId store.InternalChannelId, targets []protocol.Notification) error {
return fail
},
}
exchg := &broker.UnicastExchange{}
_, inMsg, err := exchg.Prepare(sess)
c.Assert(err, IsNil)
err = json.Unmarshal([]byte(`{"T":"ack"}`), inMsg)
c.Assert(err, IsNil)
err = exchg.Acked(sess, true)
c.Assert(err, Equals, fail)
}
func (s *exchangesSuite) TestFeedPending(c *C) {
bcast1 := json.RawMessage(`{"m": "M"}`)
decoded1 := map[string]interface{}{"m": "M"}
sess := &testing.TestBrokerSession{
Exchanges: make(chan broker.Exchange, 5),
DoGet: func(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error) {
switch chanId {
case store.SystemInternalChannelId:
return 1, help.Ns(bcast1), nil
default:
return 0, nil, nil
}
},
}
err := broker.FeedPending(sess)
c.Assert(err, IsNil)
c.Assert(len(sess.Exchanges), Equals, 2)
exchg1 := <-sess.Exchanges
c.Check(exchg1, DeepEquals, &broker.BroadcastExchange{
ChanId: store.SystemInternalChannelId,
TopLevel: 1,
Notifications: help.Ns(bcast1),
Decoded: []map[string]interface{}{decoded1},
})
exchg2 := <-sess.Exchanges
c.Check(exchg2, DeepEquals, &broker.UnicastExchange{
ChanId: sess.InternalChannelId(),
CachedOk: true,
})
}
func (s *exchangesSuite) TestFeedPendingSystemChanNop(c *C) {
bcast1 := json.RawMessage(`{"m": "M"}`)
sess := &testing.TestBrokerSession{
LevelsMap: map[store.InternalChannelId]int64{
store.SystemInternalChannelId: 1,
},
Exchanges: make(chan broker.Exchange, 5),
DoGet: func(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error) {
switch chanId {
case store.SystemInternalChannelId:
return 1, help.Ns(bcast1), nil
default:
return 0, nil, nil
}
},
}
err := broker.FeedPending(sess)
c.Assert(err, IsNil)
c.Check(len(sess.Exchanges), Equals, 1)
exchg1 := <-sess.Exchanges
c.Check(exchg1, FitsTypeOf, &broker.UnicastExchange{})
}
func (s *exchangesSuite) TestFeedPendingSystemChanFail(c *C) {
sess := &testing.TestBrokerSession{
LevelsMap: map[store.InternalChannelId]int64{
store.SystemInternalChannelId: 1,
},
Exchanges: make(chan broker.Exchange, 5),
DoGet: func(chanId store.InternalChannelId, cachedOk bool) (int64, []protocol.Notification, error) {
switch chanId {
case store.SystemInternalChannelId:
return 0, nil, errors.New("fail")
default:
return 0, nil, nil
}
},
}
err := broker.FeedPending(sess)
c.Assert(err, IsNil)
c.Check(len(sess.Exchanges), Equals, 1)
exchg1 := <-sess.Exchanges
c.Check(exchg1, FitsTypeOf, &broker.UnicastExchange{})
}
ubuntu-push-0.68+16.04.20160310.2/server/broker/broker_test.go 0000644 0000156 0000165 00000003545 12670364255 024130 0 ustar pbuser pbgroup 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/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)
}
func (s *brokerSuite) TestGetInfoInt(c *C) {
connectMsg := &protocol.ConnectMsg{}
v, err := GetInfoInt(connectMsg, "bar", -1)
c.Check(err, IsNil)
c.Check(v, Equals, -1)
err = json.Unmarshal([]byte(`{"bar": 233}`), &connectMsg.Info)
c.Assert(err, IsNil)
v, err = GetInfoInt(connectMsg, "bar", -1)
c.Check(err, IsNil)
c.Check(v, Equals, 233)
connectMsg.Info["bar"] = "garbage"
v, err = GetInfoInt(connectMsg, "bar", -1)
c.Check(err, Equals, ErrUnexpectedValue)
}
ubuntu-push-0.68+16.04.20160310.2/server/broker/exchg_impl_test.go 0000644 0000156 0000165 00000005433 12670364255 024761 0 ustar pbuser pbgroup 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"
help "launchpad.net/ubuntu-push/testing"
)
type exchangesImplSuite struct{}
var _ = Suite(&exchangesImplSuite{})
func (s *exchangesImplSuite) TestFilterByLevel(c *C) {
notifs := help.Ns(
json.RawMessage(`{"a": 3}`),
json.RawMessage(`{"a": 4}`),
json.RawMessage(`{"a": 5}`),
)
res := filterByLevel(5, 5, notifs)
c.Check(len(res), Equals, 0)
res = filterByLevel(4, 5, notifs)
c.Check(len(res), Equals, 1)
c.Check(res[0].Payload, DeepEquals, json.RawMessage(`{"a": 5}`))
res = filterByLevel(3, 5, notifs)
c.Check(len(res), Equals, 2)
c.Check(res[0].Payload, DeepEquals, json.RawMessage(`{"a": 4}`))
res = filterByLevel(2, 5, notifs)
c.Check(len(res), Equals, 3)
res = filterByLevel(1, 5, notifs)
c.Check(len(res), Equals, 3)
// too ahead, pick only last
res = filterByLevel(10, 5, notifs)
c.Check(len(res), Equals, 1)
c.Check(res[0].Payload, 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)
}
notifs := help.Ns(payloads...)
other := store.InternalChannelId("1")
c.Check(channelFilter("", store.SystemInternalChannelId, nil, nil), IsNil)
c.Check(channelFilter("", other, notifs[1:], decoded), DeepEquals, payloads[1:])
// use tag when channel is the sytem channel
c.Check(channelFilter("c/z", store.SystemInternalChannelId, notifs, decoded), HasLen, 0)
c.Check(channelFilter("a/x", store.SystemInternalChannelId, notifs, decoded), DeepEquals, []json.RawMessage{payloads[0], payloads[3]})
c.Check(channelFilter("a/x", store.SystemInternalChannelId, notifs[1:], decoded), DeepEquals, []json.RawMessage{payloads[3]})
}
ubuntu-push-0.68+16.04.20160310.2/server/broker/testsuite/ 0000755 0000156 0000165 00000000000 12670364532 023276 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/broker/testsuite/suite.go 0000644 0000156 0000165 00000032121 12670364255 024757 0 ustar pbuser pbgroup 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"
help "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
// Build a session tracker for testing.
MakeTracker func(sessionId string) broker.SessionTracker
// 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
// Let us get to a broker.UnicastExchange from an Exchange.
RevealUnicastExchange func(broker.Exchange) *broker.UnicastExchange
// private
testlog *help.TestLogger
}
func (s *CommonBrokerSuite) SetUpTest(c *C) {
s.testlog = help.NewTestLogger(c, "error")
}
var testBrokerConfig = &testing.TestBrokerConfig{10, 5}
func (s *CommonBrokerSuite) TestSanity(c *C) {
sto := store.NewInMemoryPendingStore()
b := s.MakeBroker(sto, testBrokerConfig, s.testlog)
c.Check(s.RevealSession(b, "FOO"), IsNil)
}
func (s *CommonBrokerSuite) TestStartStop(c *C) {
b := s.MakeBroker(nil, testBrokerConfig, s.testlog)
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, s.testlog)
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",
},
}, s.MakeTracker("s1"))
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: ""}, s.MakeTracker("s2"))
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, s.testlog)
b.Start()
defer b.Stop()
_, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1", Levels: map[string]int64{"z": 5}}, s.MakeTracker("s1"))
c.Check(err, FitsTypeOf, &broker.ErrAbort{})
}
func (s *CommonBrokerSuite) TestRegistrationInfoErrors(c *C) {
sto := store.NewInMemoryPendingStore()
b := s.MakeBroker(sto, testBrokerConfig, s.testlog)
b.Start()
defer b.Stop()
info := map[string]interface{}{
"device": -1,
}
_, err := b.Register(&protocol.ConnectMsg{Type: "connect", Info: info}, s.MakeTracker("s1"))
c.Check(err, Equals, broker.ErrUnexpectedValue)
info["device"] = "m"
info["channel"] = -1
_, err = b.Register(&protocol.ConnectMsg{Type: "connect", Info: info}, s.MakeTracker("s2"))
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, s.testlog)
b.Start()
defer b.Stop()
sess, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1"}, s.MakeTracker("s1"))
c.Assert(err, IsNil)
c.Check(len(sess.SessionChannel()), Equals, 2)
}
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"}, s.MakeTracker("s1"))
c.Assert(err, IsNil)
// but
c.Check(s.testlog.Captured(), Matches, "ERROR unsuccessful, get channel snapshot for 0 \\(cachedOk=true\\): get channel snapshot fail\n")
}
func clearOfPending(c *C, sess broker.BrokerSession) {
c.Assert(len(sess.SessionChannel()) >= 1, Equals, true)
<-sess.SessionChannel()
}
func (s *CommonBrokerSuite) TestRegistrationLastWins(c *C) {
sto := store.NewInMemoryPendingStore()
b := s.MakeBroker(sto, testBrokerConfig, s.testlog)
b.Start()
defer b.Stop()
sess1, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1"}, s.MakeTracker("s1"))
c.Assert(err, IsNil)
clearOfPending(c, sess1)
sess2, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1"}, s.MakeTracker("s2"))
c.Assert(err, IsNil)
// previous session got signaled by sending nil on its channel
var sentinel broker.Exchange
got := false
select {
case sentinel = <-sess1.SessionChannel():
got = true
case <-time.After(5 * time.Second):
c.Fatal("taking too long to get sentinel")
}
c.Check(got, Equals, true)
c.Check(sentinel, IsNil)
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: ""}, s.MakeTracker("s3"))
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, s.testlog)
b.Start()
defer b.Stop()
sess1, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1"}, s.MakeTracker("s1"))
c.Assert(err, IsNil)
clearOfPending(c, sess1)
sess2, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-2"}, s.MakeTracker("s2"))
c.Assert(err, IsNil)
clearOfPending(c, sess2)
// 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,
Notifications: help.Ns(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,
Notifications: help.Ns(notification1),
Decoded: []map[string]interface{}{decoded1},
})
}
}
type testFailingStore struct {
store.InMemoryPendingStore
countdownToFail int
}
func (sto *testFailingStore) GetChannelSnapshot(chanId store.InternalChannelId) (int64, []protocol.Notification, error) {
if sto.countdownToFail == 0 {
return 0, nil, errors.New("get channel snapshot fail")
}
sto.countdownToFail--
return 0, nil, nil
}
func (sto *testFailingStore) DropByMsgId(chanId store.InternalChannelId, targets []protocol.Notification) error {
return errors.New("drop fail")
}
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()
sess, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev-1"}, s.MakeTracker("s1"))
c.Assert(err, IsNil)
clearOfPending(c, sess)
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.*: get channel snapshot fail\n")
}
func (s *CommonBrokerSuite) TestUnicast(c *C) {
sto := store.NewInMemoryPendingStore()
notification1 := json.RawMessage(`{"m": "M1"}`)
notification2 := json.RawMessage(`{"m": "M2"}`)
chanId1 := store.UnicastInternalChannelId("dev1", "dev1")
chanId2 := store.UnicastInternalChannelId("dev2", "dev2")
b := s.MakeBroker(sto, testBrokerConfig, s.testlog)
b.Start()
defer b.Stop()
sess1, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev1"}, s.MakeTracker("s1"))
c.Assert(err, IsNil)
clearOfPending(c, sess1)
sess2, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev2"}, s.MakeTracker("s2"))
c.Assert(err, IsNil)
clearOfPending(c, sess2)
// add notification to channel *after* the registrations
muchLater := store.Metadata{Expiration: time.Now().Add(10 * time.Minute)}
sto.AppendToUnicastChannel(chanId1, "app1", notification1, "msg1", muchLater)
sto.AppendToUnicastChannel(chanId2, "app2", notification2, "msg2", muchLater)
b.Unicast(chanId2, chanId1)
select {
case <-time.After(5 * time.Second):
c.Fatal("taking too long to get unicast exchange")
case exchg1 := <-sess1.SessionChannel():
u1 := s.RevealUnicastExchange(exchg1)
c.Check(u1.ChanId, Equals, chanId1)
}
select {
case <-time.After(5 * time.Second):
c.Fatal("taking too long to get unicast exchange")
case exchg2 := <-sess2.SessionChannel():
u2 := s.RevealUnicastExchange(exchg2)
c.Check(u2.ChanId, Equals, chanId2)
}
}
func (s *CommonBrokerSuite) TestGetAndDrop(c *C) {
sto := store.NewInMemoryPendingStore()
notification1 := json.RawMessage(`{"m": "M1"}`)
chanId1 := store.UnicastInternalChannelId("dev3", "dev3")
b := s.MakeBroker(sto, testBrokerConfig, s.testlog)
b.Start()
defer b.Stop()
sess1, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev3"}, s.MakeTracker("s1"))
c.Assert(err, IsNil)
muchLater := store.Metadata{Expiration: time.Now().Add(10 * time.Minute)}
sto.AppendToUnicastChannel(chanId1, "app1", notification1, "msg1", muchLater)
_, expected, err := sto.GetChannelSnapshot(chanId1)
c.Assert(err, IsNil)
_, notifs, err := sess1.Get(chanId1, false)
c.Check(notifs, HasLen, 1)
c.Check(notifs, DeepEquals, expected)
err = sess1.DropByMsgId(chanId1, notifs)
c.Assert(err, IsNil)
_, notifs, err = sess1.Get(chanId1, true)
c.Check(notifs, HasLen, 0)
_, expected, err = sto.GetChannelSnapshot(chanId1)
c.Assert(err, IsNil)
c.Check(expected, HasLen, 0)
}
func (s *CommonBrokerSuite) TestGetAndDropErrors(c *C) {
chanId1 := store.UnicastInternalChannelId("dev3", "dev3")
sto := &testFailingStore{countdownToFail: 1}
b := s.MakeBroker(sto, testBrokerConfig, s.testlog)
b.Start()
defer b.Stop()
sess1, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev3"}, s.MakeTracker("s1"))
c.Assert(err, IsNil)
_, _, err = sess1.Get(chanId1, false)
c.Assert(err, ErrorMatches, "get channel snapshot fail")
c.Check(s.testlog.Captured(), Matches, "ERROR unsuccessful, get channel snapshot for Udev3:dev3 \\(cachedOk=false\\): get channel snapshot fail\n")
s.testlog.ResetCapture()
err = sess1.DropByMsgId(chanId1, nil)
c.Assert(err, ErrorMatches, "drop fail")
c.Check(s.testlog.Captured(), Matches, "ERROR unsuccessful, drop from channel Udev3:dev3: drop fail\n")
}
func (s *CommonBrokerSuite) TestSessionFeed(c *C) {
sto := store.NewInMemoryPendingStore()
b := s.MakeBroker(sto, testBrokerConfig, s.testlog)
b.Start()
defer b.Stop()
sess1, err := b.Register(&protocol.ConnectMsg{Type: "connect", DeviceId: "dev3"}, s.MakeTracker("s1"))
c.Assert(err, IsNil)
clearOfPending(c, sess1)
bcast := &broker.BroadcastExchange{ChanId: store.SystemInternalChannelId, TopLevel: 99}
sess1.Feed(bcast)
c.Check(s.RevealBroadcastExchange(<-sess1.SessionChannel()), DeepEquals, bcast)
ucast := &broker.UnicastExchange{ChanId: store.UnicastInternalChannelId("dev21", "dev21"), CachedOk: true}
sess1.Feed(ucast)
c.Check(s.RevealUnicastExchange(<-sess1.SessionChannel()), DeepEquals, ucast)
}
ubuntu-push-0.68+16.04.20160310.2/server/listener/ 0000755 0000156 0000165 00000000000 12670364532 021606 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/listener/listener_test.go 0000644 0000156 0000165 00000016334 12670364255 025032 0 ustar pbuser pbgroup 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"
"io"
"net"
"os/exec"
"regexp"
"syscall"
"testing"
"time"
"unicode"
. "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 = 20
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) TLSServerConfig() *tls.Config {
return helpers.TestTLSServerConfig
}
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(10 * time.Second))
var buf [1]byte
for {
_, err := io.ReadFull(conn, buf[:])
if err != nil {
return err
}
// 1|2... send digit back
if unicode.IsDigit(rune(buf[0])) {
break
}
}
_, err := conn.Write(buf[:])
return err
}
func testTlsDial(addr string) (net.Conn, error) {
return tls.Dial("tcp", addr, helpers.TestTLSClientConfig)
}
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 := io.ReadFull(conn, buf[:])
c.Check(err, IsNil)
c.Check(buf[0], Equals, byte(expected))
}
type testSessionResourceManager struct {
event chan string
}
func (r *testSessionResourceManager) ConsumeConn() {
r.event <- "consume"
}
// takeNext takes a string from given channel with a 5s timeout
func takeNext(ch <-chan string) string {
select {
case <-time.After(5 * time.Second):
panic("test protocol exchange stuck: too long waiting")
case v := <-ch:
return v
}
}
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)
rEvent := make(chan string)
resource := &testSessionResourceManager{rEvent}
go func() {
errCh <- lst.AcceptLoop(testSession, resource, s.testlog)
}()
listenerAddr := lst.Addr().String()
c.Check(takeNext(rEvent), Equals, "consume")
conn1, err := testTlsDial(listenerAddr)
c.Assert(err, IsNil)
c.Check(takeNext(rEvent), Equals, "consume")
defer conn1.Close()
testWriteByte(c, conn1, '1')
conn2, err := testTlsDial(listenerAddr)
c.Assert(err, IsNil)
c.Check(takeNext(rEvent), Equals, "consume")
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, "")
}
// waitForLogs waits for the logs captured in s.testlog to match reStr.
func (s *listenerSuite) waitForLogs(c *C, reStr string) {
rx := regexp.MustCompile("^" + reStr + "$")
for i := 0; i < 100; i++ {
if rx.MatchString(s.testlog.Captured()) {
break
}
time.Sleep(20 * time.Millisecond)
}
c.Check(s.testlog.Captured(), Matches, reStr)
}
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)
resource := &NopSessionResourceManager{}
go func() {
errCh <- lst.AcceptLoop(testSession, resource, s.testlog)
}()
listenerAddr := lst.Addr().String()
connectMany := helpers.ScriptAbsPath("connect-many.py")
cmd := exec.Command(connectMany, listenerAddr)
res, err := cmd.Output()
c.Assert(err, IsNil)
c.Assert(string(res), Matches, "(?s).*timed out.*")
conn2, err := testTlsDial(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.*")
s.waitForLogs(c, "(?ms).*device listener:.*accept.*too many open.*-- retrying")
}
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)
resource := &NopSessionResourceManager{}
go func() {
errCh <- lst.AcceptLoop(func(conn net.Conn) error {
defer conn.Close()
panic("session crash")
}, resource, s.testlog)
}()
listenerAddr := lst.Addr().String()
_, err = testTlsDial(listenerAddr)
c.Assert(err, Not(IsNil))
lst.Close()
c.Check(<-errCh, ErrorMatches, ".*use of closed.*")
s.waitForLogs(c, "(?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)
resource := &NopSessionResourceManager{}
go func() {
errCh <- lst.AcceptLoop(testSession, resource, s.testlog)
}()
listenerAddr := lst.Addr().String()
c.Check(listenerAddr, Equals, foreignLst.Addr().String())
conn1, err := testTlsDial(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.68+16.04.20160310.2/server/listener/listener.go 0000644 0000156 0000165 00000005360 12670364255 023770 0 ustar pbuser pbgroup 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 config
TLSServerConfig() *tls.Config
}
// 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
}
}
tlsCfg := cfg.TLSServerConfig()
return &DeviceListener{tls.NewListener(lst, tlsCfg)}, nil
}
// 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
}
// SessionResourceManager allows to limit resource usage tracking connections.
type SessionResourceManager interface {
ConsumeConn()
}
// NOP SessionResourceManager.
type NopSessionResourceManager struct{}
func (r *NopSessionResourceManager) ConsumeConn() {}
// AcceptLoop accepts connections and starts sessions for them.
func (dl *DeviceListener) AcceptLoop(session func(net.Conn) error, resource SessionResourceManager, logger logger.Logger) error {
for {
resource.ConsumeConn()
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.68+16.04.20160310.2/server/session/ 0000755 0000156 0000165 00000000000 12670364532 021444 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/session/tracker_test.go 0000644 0000156 0000165 00000004170 12670364255 024471 0 ustar pbuser pbgroup 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.SessionId(), Not(Equals), "")
regExpected := fmt.Sprintf(`DEBUG session\(%s\) connected 127\.0\.0\.1:9999\n`, track.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\(%s\) registered DEV-ID\n`, track.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\(%s\) ended with: session aborted \(\)\n`, track.SessionId())
c.Check(s.testlog.Captured(), Matches, regExpected)
}
ubuntu-push-0.68+16.04.20160310.2/server/session/tracker.go 0000644 0000156 0000165 00000004142 12670364255 023431 0 ustar pbuser pbgroup 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"
"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)
// SessionId
SessionId() string
// 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 string
}
func NewTracker(logger logger.Logger) SessionTracker {
return &tracker{Logger: logger}
}
func (trk *tracker) Start(conn WithRemoteAddr) {
trk.sessionId = fmt.Sprintf("%x", time.Now().UnixNano()-sessionsEpoch)
trk.Debugf("session(%s) connected %v", trk.sessionId, conn.RemoteAddr())
}
func (trk *tracker) SessionId() string {
return trk.sessionId
}
func (trk *tracker) Registered(sess broker.BrokerSession) {
trk.Infof("session(%s) registered %v", trk.sessionId, sess.DeviceIdentifier())
}
func (trk *tracker) EffectivePingInterval(time.Duration) {
}
func (trk *tracker) End(err error) error {
trk.Debugf("session(%s) ended with: %v", trk.sessionId, err)
return err
}
ubuntu-push-0.68+16.04.20160310.2/server/session/session_test.go 0000644 0000156 0000165 00000061057 12670364255 024530 0 ustar pbuser pbgroup 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, track broker.SessionTracker) (broker.BrokerSession, error) {
sessionId := track.SessionId()
tb.registration <- fmt.Sprintf("register %s %s", connect.DeviceId, sessionId)
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, &tracker{sessionId: "s1"})
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 s1")
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, &tracker{sessionId: "s2"})
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, &tracker{sessionId: "s3"})
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, &tracker{sessionId: "s4"})
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, &tracker{sessionId: "s5"})
c.Check(err, DeepEquals, &broker.ErrAbort{"expected CONNECT message"})
}
func (s *sessionSuite) TestSessionLoop(c *C) {
track := &testTracker{NewTracker(s.testlog), make(chan interface{}, 2)}
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, cfg10msPingInterval5msExchangeTout, track)
}()
c.Check(takeNext(down), Equals, "deadline 5ms")
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 5ms")
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)
c.Check(track.interval, HasLen, 2)
// TODO: Fix racyness. See lp:1522880
// c.Check((<-track.interval).(time.Duration) <= 16*time.Millisecond, Equals, true)
// c.Check((<-track.interval).(time.Duration) <= 16*time.Millisecond, Equals, true)
}
var cfg5msPingInterval2msExchangeTout = &testSessionConfig{
pingInterval: 5 * time.Millisecond,
exchangeTimeout: 2 * time.Millisecond,
}
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)
}()
exchanges <- nil
err := <-errCh
c.Check(err, DeepEquals, &broker.ErrAbort{"terminated"})
}
func (s *sessionSuite) TestPingTimerReset(c *C) {
pingInterval := 50 * time.Millisecond
now := time.Now()
l := &loop{
pingInterval: pingInterval,
pingTimer: time.NewTimer(pingInterval),
intervalStart: now,
}
time.Sleep(10 * time.Millisecond)
l.pingTimer.Stop()
done := l.pingTimerReset(true)
c.Assert(done, Equals, true)
select {
case <-l.pingTimer.C:
case <-time.After(1 * time.Second):
c.Fatal("timer should have triggered")
}
c.Check(now, Not(Equals), l.intervalStart)
}
func (s *sessionSuite) TestPingTimerResetPartial(c *C) {
pingInterval := 5 * time.Second
now := time.Now().Add(-pingInterval + 50*time.Millisecond)
l := &loop{
pingInterval: pingInterval,
pingTimer: time.NewTimer(pingInterval),
intervalStart: now,
}
l.pingTimer.Stop()
done := l.pingTimerReset(false)
c.Assert(done, Equals, true)
select {
case <-l.pingTimer.C:
case <-time.After(1 * time.Second):
c.Fatal("timer should have triggered")
}
c.Check(now, Equals, l.intervalStart)
}
func (s *sessionSuite) TestPingTimerResetPartialTooLate(c *C) {
pingInterval := 5 * time.Second
now := time.Now().Add(-pingInterval - 50*time.Millisecond)
l := &loop{
pingInterval: pingInterval,
pingTimer: time.NewTimer(pingInterval),
intervalStart: now,
}
l.pingTimer.Stop()
done := l.pingTimerReset(false)
c.Assert(done, Equals, false)
}
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}
pingInterval := 5 * time.Second
go func() {
l := &loop{
proto: tp,
sess: sess,
track: nopTrack,
pingInterval: pingInterval,
pingTimer: time.NewTimer(pingInterval),
exchangeTimeout: 1 * time.Second,
intervalStart: time.Now().Add(-pingInterval + 50*time.Millisecond),
}
errCh <- l.run()
}()
c.Check(takeNext(down), Equals, "deadline 1s")
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) TestSessionLoopExchangeErrNopNeedPing(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}
pingInterval := 5 * time.Second
go func() {
l := &loop{
proto: tp,
sess: sess,
track: nopTrack,
pingInterval: pingInterval,
pingTimer: time.NewTimer(pingInterval),
exchangeTimeout: 1 * time.Second,
intervalStart: time.Now().Add(-pingInterval - 50*time.Millisecond),
}
errCh <- l.run()
}()
c.Check(takeNext(down), Equals, "deadline 1s")
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)
msg := &protocol.ConnBrokenMsg{"connbroken", "BREASON"}
exchanges <- &broker.ConnMetaExchange{msg}
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", "BREASON"})
up <- nil // no write error
err := <-errCh
c.Check(err, DeepEquals, &broker.ErrAbort{"session broken for reason"})
}
func (s *sessionSuite) TestSessionLoopConnWarnExchange(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)
msg := &protocol.ConnWarnMsg{"connwarn", "WREASON"}
exchanges <- &broker.ConnMetaExchange{msg}
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.ConnWarnMsg{"connwarn", "WREASON"})
up <- nil // no write error
// session continues
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)
}
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.19, 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 "+track.SessionId())
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.68+16.04.20160310.2/server/session/session.go 0000644 0000156 0000165 00000012314 12670364255 023461 0 ustar pbuser pbgroup 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 (
"errors"
"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, track SessionTracker) (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, track)
}
var errOneway = errors.New("oneway")
type loop struct {
// params
proto protocol.Protocol
sess broker.BrokerSession
track SessionTracker
// exchange timeout
exchangeTimeout time.Duration
// ping mgmt
pingInterval time.Duration
pingTimer *time.Timer
intervalStart time.Time
}
// exchange writes outMsg message, reads answer in inMsg
func (l *loop) exchange(outMsg, inMsg interface{}) error {
proto := l.proto
proto.SetDeadline(time.Now().Add(l.exchangeTimeout))
err := proto.WriteMessage(outMsg)
if err != nil {
return err
}
if inMsg == nil { // no answer expected
if outMsg.(protocol.OnewayMsg).OnewayContinue() {
return errOneway
}
return &broker.ErrAbort{"session broken for reason"}
}
err = proto.ReadMessage(inMsg)
if err != nil {
return err
}
return nil
}
func (l *loop) pingTimerReset(totalReset bool) bool {
interval := l.pingInterval
if !totalReset {
interval -= time.Since(l.intervalStart)
if interval <= 0 {
return false // late
}
}
l.pingTimer.Reset(interval)
if totalReset {
l.intervalStart = time.Now()
}
return true
}
func (l *loop) doPing() error {
l.track.EffectivePingInterval(time.Since(l.intervalStart))
pingMsg := &protocol.PingPongMsg{"ping"}
var pongMsg protocol.PingPongMsg
err := l.exchange(pingMsg, &pongMsg)
if err != nil {
return err
}
if pongMsg.Type != "pong" {
return &broker.ErrAbort{"expected PONG message"}
}
l.pingTimerReset(true)
return nil
}
func (l *loop) run() error {
ch := l.sess.SessionChannel()
Loop:
for {
select {
case <-l.pingTimer.C:
err := l.doPing()
if err != nil {
return err
}
case exchg := <-ch:
l.pingTimer.Stop()
if exchg == nil {
return &broker.ErrAbort{"terminated"}
}
outMsg, inMsg, err := exchg.Prepare(l.sess)
if err == broker.ErrNop { // nothing to do
if !l.pingTimerReset(false) {
// we are late, do a ping here
err := l.doPing()
if err != nil {
return err
}
}
continue Loop
}
if err != nil {
return err
}
for {
done := outMsg.Split()
err = l.exchange(outMsg, inMsg)
if err == errOneway {
l.pingTimerReset(true)
continue Loop
}
if err != nil {
return err
}
if done {
l.pingTimerReset(true)
}
err = exchg.Acked(l.sess, done)
if err != nil {
return err
}
if done {
break
}
}
}
}
}
// sessionLoop manages the exchanges of the protocol session.
func sessionLoop(proto protocol.Protocol, sess broker.BrokerSession, cfg SessionConfig, track SessionTracker) error {
pingInterval := cfg.PingInterval()
l := &loop{
proto: proto,
sess: sess,
track: track,
// ping setup
pingInterval: pingInterval,
pingTimer: time.NewTimer(pingInterval),
intervalStart: time.Now(),
exchangeTimeout: cfg.ExchangeTimeout(),
}
return l.run()
}
// 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, track)
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.68+16.04.20160310.2/server/store/ 0000755 0000156 0000165 00000000000 12670364532 021115 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/store/inmemory_test.go 0000644 0000156 0000165 00000033216 12670364255 024351 0 ustar pbuser pbgroup 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"
"launchpad.net/ubuntu-push/protocol"
help "launchpad.net/ubuntu-push/testing"
)
type inMemorySuite struct{}
var _ = Suite(&inMemorySuite{})
func (s *inMemorySuite) TestRegister(c *C) {
sto := NewInMemoryPendingStore()
tok1, err := sto.Register("DEV1", "app1")
c.Assert(err, IsNil)
tok2, err := sto.Register("DEV1", "app1")
c.Assert(err, IsNil)
c.Check(len(tok1), Not(Equals), 0)
c.Check(tok1, Equals, tok2)
}
func (s *inMemorySuite) TestUnregister(c *C) {
sto := NewInMemoryPendingStore()
err := sto.Unregister("DEV1", "app1")
c.Assert(err, IsNil)
}
func (s *inMemorySuite) TestGetInternalChannelIdFromToken(c *C) {
sto := NewInMemoryPendingStore()
tok1, err := sto.Register("DEV1", "app1")
c.Assert(err, IsNil)
chanId, err := sto.GetInternalChannelIdFromToken(tok1, "app1", "", "")
c.Assert(err, IsNil)
c.Check(chanId, Equals, UnicastInternalChannelId("DEV1", "DEV1"))
}
func (s *inMemorySuite) TestGetInternalChannelIdFromTokenFallback(c *C) {
sto := NewInMemoryPendingStore()
chanId, err := sto.GetInternalChannelIdFromToken("", "app1", "u1", "d1")
c.Assert(err, IsNil)
c.Check(chanId, Equals, UnicastInternalChannelId("u1", "d1"))
}
func (s *inMemorySuite) TestGetInternalChannelIdFromTokenErrors(c *C) {
sto := NewInMemoryPendingStore()
tok1, err := sto.Register("DEV1", "app1")
c.Assert(err, IsNil)
_, err = sto.GetInternalChannelIdFromToken(tok1, "app2", "", "")
c.Assert(err, Equals, ErrUnauthorized)
_, err = sto.GetInternalChannelIdFromToken("", "app2", "", "")
c.Assert(err, Equals, ErrUnknownToken)
_, err = sto.GetInternalChannelIdFromToken("****", "app2", "", "")
c.Assert(err, Equals, ErrUnknownToken)
}
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, []protocol.Notification(nil))
}
func (s *inMemorySuite) TestGetChannelUnfilteredEmpty(c *C) {
sto := NewInMemoryPendingStore()
top, res, meta, err := sto.GetChannelUnfiltered(SystemInternalChannelId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(0))
c.Check(res, DeepEquals, []protocol.Notification(nil))
c.Check(meta, DeepEquals, []Metadata(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, help.Ns(notification1, notification2))
}
func (s *inMemorySuite) TestAppendToUnicastChannelAndGetChannelSnapshot(c *C) {
sto := NewInMemoryPendingStore()
chanId := UnicastInternalChannelId("user", "dev1")
notification1 := json.RawMessage(`{"a":1}`)
notification2 := json.RawMessage(`{"b":2}`)
muchLater := Metadata{Expiration: time.Now().Add(time.Minute)}
err := sto.AppendToUnicastChannel(chanId, "app1", notification1, "m1", muchLater)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app2", notification2, "m2", muchLater)
c.Assert(err, IsNil)
top, res, err := sto.GetChannelSnapshot(chanId)
c.Assert(err, IsNil)
c.Check(res, DeepEquals, []protocol.Notification{
protocol.Notification{Payload: notification1, AppId: "app1", MsgId: "m1"},
protocol.Notification{Payload: notification2, AppId: "app2", MsgId: "m2"},
})
c.Check(top, Equals, int64(0))
}
func (s *inMemorySuite) TestAppendToChannelAndGetChannelUnfiltered(c *C) {
sto := NewInMemoryPendingStore()
notification1 := json.RawMessage(`{"a":1}`)
notification2 := json.RawMessage(`{"a":2}`)
gone := time.Now().Add(-1 * time.Minute)
muchLater := time.Now().Add(time.Minute)
sto.AppendToChannel(SystemInternalChannelId, notification1, muchLater)
sto.AppendToChannel(SystemInternalChannelId, notification2, gone)
top, res, meta, err := sto.GetChannelUnfiltered(SystemInternalChannelId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(2))
c.Check(res, DeepEquals, help.Ns(notification1, notification2))
c.Check(meta, DeepEquals, []Metadata{
Metadata{Expiration: muchLater},
Metadata{Expiration: gone},
})
}
func (s *inMemorySuite) TestAppendToUnicastChannelReplaceTagAndGetChannelUnfiltered(c *C) {
sto := NewInMemoryPendingStore()
chanId := UnicastInternalChannelId("user", "dev1")
notification1 := json.RawMessage(`{"a":1}`)
notification2 := json.RawMessage(`{"a":2}`)
meta1 := Metadata{Expiration: time.Now().Add(2 * time.Minute)}
meta2 := Metadata{
Expiration: time.Now().Add(3 * time.Minute),
ReplaceTag: "u1",
}
sto.AppendToUnicastChannel(chanId, "app1", notification1, "m1", meta1)
sto.AppendToUnicastChannel(chanId, "app2", notification2, "m2", meta2)
top, res, meta, err := sto.GetChannelUnfiltered(chanId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(0))
c.Check(res, DeepEquals, []protocol.Notification{
protocol.Notification{Payload: notification1, AppId: "app1", MsgId: "m1"},
protocol.Notification{Payload: notification2, AppId: "app2", MsgId: "m2"},
})
c.Check(meta, DeepEquals, []Metadata{meta1, meta2})
}
func (s *inMemorySuite) TestAppendToChannelAndGetChannelSnapshotWithExpiration(c *C) {
sto := NewInMemoryPendingStore()
notification1 := json.RawMessage(`{"a":1}`)
notification2 := json.RawMessage(`{"a":2}`)
gone := time.Now().Add(-1 * time.Minute)
muchLater := time.Now().Add(time.Minute)
sto.AppendToChannel(SystemInternalChannelId, notification1, muchLater)
sto.AppendToChannel(SystemInternalChannelId, notification2, gone)
top, res, err := sto.GetChannelSnapshot(SystemInternalChannelId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(2))
c.Check(res, DeepEquals, help.Ns(notification1))
}
func (s *inMemorySuite) TestAppendToUnicastChannelAndGetChannelSnapshotWithExpirationAndCoalescing(c *C) {
sto := NewInMemoryPendingStore()
chanId := UnicastInternalChannelId("user", "dev1")
notification1 := json.RawMessage(`{"a":1}`)
notification2 := json.RawMessage(`{"a":2}`)
notification3 := json.RawMessage(`{"a":4}`)
notification4 := json.RawMessage(`{"a":4}`)
meta1 := Metadata{
Expiration: time.Now().Add(1 * time.Minute),
ReplaceTag: "u1",
}
meta2 := Metadata{Expiration: time.Now().Add(-1 * time.Minute)}
meta3 := Metadata{
Expiration: time.Now().Add(1 * time.Minute),
ReplaceTag: "u1",
}
meta4 := Metadata{Expiration: time.Now().Add(1 * time.Minute)}
err := sto.AppendToUnicastChannel(chanId, "app1", notification1, "m1", meta1)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app1", notification2, "m2", meta2)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app1", notification3, "m3", meta3)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app1", notification4, "m4", meta4)
c.Assert(err, IsNil)
top, res, err := sto.GetChannelSnapshot(chanId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(0))
c.Check(res, DeepEquals, []protocol.Notification{
protocol.Notification{Payload: notification3, AppId: "app1", MsgId: "m3"},
protocol.Notification{Payload: notification4, AppId: "app1", MsgId: "m4"},
})
}
func (s *inMemorySuite) TestScrubNop(c *C) {
sto := NewInMemoryPendingStore()
chanId := UnicastInternalChannelId("user", "dev1")
err := sto.Scrub(chanId)
c.Assert(err, IsNil)
}
func (s *inMemorySuite) TestScrubMax2Criteria(c *C) {
sto := NewInMemoryPendingStore()
chanId := UnicastInternalChannelId("user", "dev1")
c.Check(func() { sto.Scrub(chanId, "a", "b", "c") }, PanicMatches, `Scrub\(\) expects only up to two criterias`)
}
func (s *inMemorySuite) TestScrubOnlyExpired(c *C) {
sto := NewInMemoryPendingStore()
chanId := UnicastInternalChannelId("user", "dev1")
notification1 := json.RawMessage(`{"a":1}`)
notification2 := json.RawMessage(`{"b":2}`)
notification3 := json.RawMessage(`{"c":3}`)
notification4 := json.RawMessage(`{"d":4}`)
gone := Metadata{Expiration: time.Now().Add(-1 * time.Minute)}
muchLater1 := Metadata{Expiration: time.Now().Add(4 * time.Minute)}
muchLater2 := Metadata{Expiration: time.Now().Add(5 * time.Minute)}
err := sto.AppendToUnicastChannel(chanId, "app1", notification1, "m1", muchLater1)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app2", notification2, "m2", gone)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app1", notification3, "m3", gone)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app2", notification4, "m4", muchLater2)
c.Assert(err, IsNil)
err = sto.Scrub(chanId)
c.Assert(err, IsNil)
top, res, meta, err := sto.GetChannelUnfiltered(chanId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(0))
c.Check(res, DeepEquals, []protocol.Notification{
protocol.Notification{Payload: notification1, AppId: "app1", MsgId: "m1"},
protocol.Notification{Payload: notification4, AppId: "app2", MsgId: "m4"},
})
c.Check(meta, DeepEquals, []Metadata{muchLater1, muchLater2})
}
func (s *inMemorySuite) TestScrubApp(c *C) {
sto := NewInMemoryPendingStore()
chanId := UnicastInternalChannelId("user", "dev1")
notification1 := json.RawMessage(`{"a":1}`)
notification2 := json.RawMessage(`{"b":2}`)
notification3 := json.RawMessage(`{"c":3}`)
notification4 := json.RawMessage(`{"d":4}`)
gone := Metadata{Expiration: time.Now().Add(-1 * time.Minute)}
muchLater := Metadata{Expiration: time.Now().Add(time.Minute)}
err := sto.AppendToUnicastChannel(chanId, "app1", notification1, "m1", muchLater)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app2", notification2, "m2", gone)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app1", notification3, "m3", muchLater)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app2", notification4, "m4", muchLater)
c.Assert(err, IsNil)
err = sto.Scrub(chanId, "app1")
c.Assert(err, IsNil)
top, res, meta, err := sto.GetChannelUnfiltered(chanId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(0))
c.Check(res, DeepEquals, []protocol.Notification{
protocol.Notification{Payload: notification4, AppId: "app2", MsgId: "m4"},
})
c.Check(meta, DeepEquals, []Metadata{muchLater})
}
func (s *inMemorySuite) TestScrubReplaceTag(c *C) {
sto := NewInMemoryPendingStore()
chanId := UnicastInternalChannelId("user", "dev1")
notification1 := json.RawMessage(`{"a":1}`)
notification2 := json.RawMessage(`{"a":2}`)
notification3 := json.RawMessage(`{"a":4}`)
notification4 := json.RawMessage(`{"a":4}`)
meta1 := Metadata{
Expiration: time.Now().Add(1 * time.Minute),
ReplaceTag: "u1",
}
meta2 := Metadata{Expiration: time.Now().Add(-1 * time.Minute)}
meta3 := Metadata{
Expiration: time.Now().Add(1 * time.Minute),
ReplaceTag: "u1",
}
meta4 := Metadata{
Expiration: time.Now().Add(1 * time.Minute),
ReplaceTag: "u2",
}
err := sto.AppendToUnicastChannel(chanId, "app1", notification1, "m1", meta1)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app1", notification2, "m2", meta2)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app1", notification3, "m3", meta3)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app1", notification4, "m4", meta4)
c.Assert(err, IsNil)
err = sto.Scrub(chanId, "app1", "u1")
c.Assert(err, IsNil)
top, res, meta, err := sto.GetChannelUnfiltered(chanId)
c.Assert(err, IsNil)
c.Check(top, Equals, int64(0))
c.Check(res, DeepEquals, []protocol.Notification{
protocol.Notification{Payload: notification4, AppId: "app1", MsgId: "m4"},
})
c.Check(meta, DeepEquals, []Metadata{meta4})
}
func (s *inMemorySuite) TestDropByMsgId(c *C) {
sto := NewInMemoryPendingStore()
chanId := UnicastInternalChannelId("user", "dev2")
// nothing to do is fine
err := sto.DropByMsgId(chanId, nil)
c.Assert(err, IsNil)
notification1 := json.RawMessage(`{"a":1}`)
notification2 := json.RawMessage(`{"b":2}`)
notification3 := json.RawMessage(`{"a":2}`)
muchLater := Metadata{Expiration: time.Now().Add(time.Minute)}
err = sto.AppendToUnicastChannel(chanId, "app1", notification1, "m1", muchLater)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app2", notification2, "m2", muchLater)
c.Assert(err, IsNil)
err = sto.AppendToUnicastChannel(chanId, "app1", notification3, "m3", muchLater)
c.Assert(err, IsNil)
_, res, err := sto.GetChannelSnapshot(chanId)
c.Assert(err, IsNil)
err = sto.DropByMsgId(chanId, res[:2])
c.Assert(err, IsNil)
_, res, err = sto.GetChannelSnapshot(chanId)
c.Assert(err, IsNil)
c.Check(res, HasLen, 1)
c.Check(res, DeepEquals, []protocol.Notification{
protocol.Notification{Payload: notification3, AppId: "app1", MsgId: "m3"},
})
}
ubuntu-push-0.68+16.04.20160310.2/server/store/store.go 0000644 0000156 0000165 00000015661 12670364255 022613 0 ustar pbuser pbgroup 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"
"fmt"
"strings"
"time"
"launchpad.net/ubuntu-push/protocol"
)
type InternalChannelId string
// BroadcastChannel returns whether the id represents a broadcast channel.
func (icid InternalChannelId) BroadcastChannel() bool {
marker := icid[0]
return marker == 'B' || marker == '0'
}
// UnicastChannel returns whether the id represents a unicast channel.
func (icid InternalChannelId) UnicastChannel() bool {
marker := icid[0]
return marker == 'U'
}
// UnicastUserAndDevice returns the user and device ids of a unicast channel.
func (icid InternalChannelId) UnicastUserAndDevice() (userId, deviceId string) {
if !icid.UnicastChannel() {
panic("UnicastUserAndDevice is for unicast channels")
}
parts := strings.SplitN(string(icid)[1:], ":", 2)
return parts[0], parts[1]
}
var ErrUnknownChannel = errors.New("unknown channel name")
var ErrUnknownToken = errors.New("unknown token")
var ErrUnauthorized = errors.New("unauthorized")
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"
}
if !chanId.BroadcastChannel() {
panic("InternalChannelIdToHex is for broadcast channels")
}
return string(chanId)[1:]
}
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
}
// mark with B(roadcast) prefix
s := "B" + hexRepr
return InternalChannelId(s), nil
}
// UnicastInternalChannelId builds a channel id for the userId, deviceId pair.
func UnicastInternalChannelId(userId, deviceId string) InternalChannelId {
return InternalChannelId(fmt.Sprintf("U%s:%s", userId, deviceId))
}
// Metadata holds the metadata stored for a notification.
type Metadata struct {
Expiration time.Time
ReplaceTag string
Obsolete bool
}
// Before checks whether the expiration date in the metadata is before ref.
func (m *Metadata) Before(ref time.Time) bool {
return m.Expiration.Before(ref)
}
// PendingStore let store notifications into channels.
type PendingStore interface {
// Register returns a token for a device id, application id pair.
Register(deviceId, appId string) (token string, err error)
// Unregister forgets the token for a device id, application id pair.
Unregister(deviceId, appId string) error
// 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
// GetInternalChannelIdFromToken returns the matching internal store
// id for a channel given a registered token and application id or
// directly a device id, user id pair.
GetInternalChannelIdFromToken(token, appId, userId, deviceId string) (InternalChannelId, error)
// AppendToUnicastChannel appends a notification to the unicast channel.
AppendToUnicastChannel(chanId InternalChannelId, appId string, notification json.RawMessage, msgId string, meta Metadata) error
// GetChannelSnapshot gets all the current notifications and
// current top level in the channel.
GetChannelSnapshot(chanId InternalChannelId) (topLevel int64, notifications []protocol.Notification, err error)
// GetChannelUnfiltered gets all the stored notifications with
// metadata and current top level in the channel.
GetChannelUnfiltered(chanId InternalChannelId) (topLevel int64, notifications []protocol.Notification, metadata []Metadata, err error)
// Scrub removes notifications from the channel based on criteria.
// Usages:
// Scrub(chanId) removes all expired notifications.
// Scrub(chanId, appId) removes all expired notifications and
// all notifications for appId.
// Scrub(chanId, appId, replaceTag) removes all expired notifications
// and all notifications matching both appId and replaceTag.
Scrub(chanId InternalChannelId, criteria ...string) error
// DropByMsgId drops notifications from a unicast channel
// based on message ids.
DropByMsgId(chanId InternalChannelId, targets []protocol.Notification) error
// Close is to be called when done with the store.
Close()
}
// FilterOutByMsgId returns the notifications from orig whose msg id is not
// mentioned in targets.
func FilterOutByMsgId(orig, targets []protocol.Notification) []protocol.Notification {
n := len(orig)
t := len(targets)
// common case, removing the continuous head
if t > 0 && n >= t {
if targets[0].MsgId == orig[0].MsgId {
for i := t - 1; i >= 0; i-- {
if i == 0 {
return orig[t:]
}
if targets[i].MsgId != orig[i].MsgId {
break
}
}
}
}
// slow way
ids := make(map[string]bool, t)
for _, target := range targets {
ids[target.MsgId] = true
}
acc := make([]protocol.Notification, 0, n)
for _, notif := range orig {
if !ids[notif.MsgId] {
acc = append(acc, notif)
}
}
return acc
}
type tagKey struct {
appId, replaceTag string
}
// FilterOutObsolete filters out expired notifications and superseded
// notifications sharing a replace tag based on paired meta
// information.
func FilterOutObsolete(notifications []protocol.Notification, meta []Metadata) []protocol.Notification {
now := time.Now()
seenTags := make(map[tagKey]bool, 10)
n := 0
// walk backward to keep the latest ones with a given ReplaceTag
for j := len(meta) - 1; j >= 0; j-- {
if meta[j].Before(now) {
meta[j].Obsolete = true
continue
}
if meta[j].ReplaceTag != "" {
key := tagKey{notifications[j].AppId, meta[j].ReplaceTag}
seen := seenTags[key]
if seen {
meta[j].Obsolete = true
continue
} else {
seenTags[key] = true
}
}
n++
}
res := make([]protocol.Notification, n)
j := 0
for i := range meta {
if !meta[i].Obsolete {
res[j] = notifications[i]
j++
}
}
return res
}
ubuntu-push-0.68+16.04.20160310.2/server/store/store_test.go 0000644 0000156 0000165 00000006642 12670364255 023651 0 ustar pbuser pbgroup 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)
c.Check(InternalChannelIdToHex(InternalChannelId("Bf1c9bf7096084cb2a154979ce00c7f50")), Equals, "f1c9bf7096084cb2a154979ce00c7f50")
c.Check(func() { InternalChannelIdToHex(InternalChannelId("U")) }, PanicMatches, "InternalChannelIdToHex is for broadcast channels")
}
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)
c.Check(i1.BroadcastChannel(), Equals, true)
i2, err := HexToInternalChannelId("f1c9bf7096084cb2a154979ce00c7f50")
c.Check(err, IsNil)
c.Check(i2.BroadcastChannel(), Equals, true)
c.Check(i2, Equals, InternalChannelId("Bf1c9bf7096084cb2a154979ce00c7f50"))
_, err = HexToInternalChannelId("01")
c.Check(err, Equals, ErrExpected128BitsHexRepr)
_, err = HexToInternalChannelId("abceddddddddddddddddzeeeeeeeeeee")
c.Check(err, Equals, ErrExpected128BitsHexRepr)
_, err = HexToInternalChannelId("f1c9bf7096084cb2a154979ce00c7f50ff")
c.Check(err, Equals, ErrExpected128BitsHexRepr)
}
func (s *storeSuite) TestUnicastInternalChannelId(c *C) {
chanId := UnicastInternalChannelId("user1", "dev2")
c.Check(chanId.BroadcastChannel(), Equals, false)
c.Check(chanId.UnicastChannel(), Equals, true)
u, d := chanId.UnicastUserAndDevice()
c.Check(u, Equals, "user1")
c.Check(d, Equals, "dev2")
c.Check(func() { SystemInternalChannelId.UnicastUserAndDevice() }, PanicMatches, "UnicastUserAndDevice is for unicast channels")
}
func (s *storeSuite) TestFilterOutByMsgId(c *C) {
orig := []protocol.Notification{
protocol.Notification{MsgId: "a"},
protocol.Notification{MsgId: "b"},
protocol.Notification{MsgId: "c"},
protocol.Notification{MsgId: "d"},
}
// removing the continuous head
res := FilterOutByMsgId(orig, orig[:3])
c.Check(res, DeepEquals, orig[3:])
// random removal
res = FilterOutByMsgId(orig, orig[1:2])
c.Check(res, DeepEquals, []protocol.Notification{
protocol.Notification{MsgId: "a"},
protocol.Notification{MsgId: "c"},
protocol.Notification{MsgId: "d"},
})
// looks like removing the continuous head, but it isn't
res = FilterOutByMsgId(orig, []protocol.Notification{
protocol.Notification{MsgId: "a"},
protocol.Notification{MsgId: "c"},
protocol.Notification{MsgId: "d"},
})
c.Check(res, DeepEquals, []protocol.Notification{
protocol.Notification{MsgId: "b"},
})
}
ubuntu-push-0.68+16.04.20160310.2/server/store/inmemory.go 0000644 0000156 0000165 00000014220 12670364255 023304 0 ustar pbuser pbgroup 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/base64"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"launchpad.net/ubuntu-push/protocol"
)
// one stored channel
type channel struct {
topLevel int64
notifications []protocol.Notification
meta []Metadata
}
// 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) Register(deviceId, appId string) (string, error) {
return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s::%s", appId, deviceId))), nil
}
func (sto *InMemoryPendingStore) Unregister(deviceId, appId string) error {
// do nothing, tokens here are computed deterministically and not stored
return nil
}
func (sto *InMemoryPendingStore) GetInternalChannelIdFromToken(token, appId, userId, deviceId string) (InternalChannelId, error) {
if token != "" && appId != "" {
decoded, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return "", ErrUnknownToken
}
token = string(decoded)
if !strings.HasPrefix(token, appId+"::") {
return "", ErrUnauthorized
}
deviceId := token[len(appId)+2:]
return UnicastInternalChannelId(deviceId, deviceId), nil
}
if userId != "" && deviceId != "" {
return UnicastInternalChannelId(userId, deviceId), nil
}
return "", ErrUnknownToken
}
func (sto *InMemoryPendingStore) GetInternalChannelId(name string) (InternalChannelId, error) {
if name == "system" {
return SystemInternalChannelId, nil
}
return InternalChannelId(""), ErrUnknownChannel
}
func (sto *InMemoryPendingStore) appendToChannel(chanId InternalChannelId, newNotification protocol.Notification, inc int64, meta1 Metadata) error {
sto.lock.Lock()
defer sto.lock.Unlock()
prev := sto.store[chanId]
if prev == nil {
prev = &channel{}
}
prev.topLevel += inc
prev.notifications = append(prev.notifications, newNotification)
prev.meta = append(prev.meta, meta1)
sto.store[chanId] = prev
return nil
}
func (sto *InMemoryPendingStore) AppendToChannel(chanId InternalChannelId, notificationPayload json.RawMessage, expiration time.Time) error {
newNotification := protocol.Notification{Payload: notificationPayload}
meta1 := Metadata{Expiration: expiration}
return sto.appendToChannel(chanId, newNotification, 1, meta1)
}
func (sto *InMemoryPendingStore) AppendToUnicastChannel(chanId InternalChannelId, appId string, notificationPayload json.RawMessage, msgId string, meta Metadata) error {
newNotification := protocol.Notification{
Payload: notificationPayload,
AppId: appId,
MsgId: msgId,
}
return sto.appendToChannel(chanId, newNotification, 0, meta)
}
func (sto *InMemoryPendingStore) getChannelUnfiltered(chanId InternalChannelId) (*channel, []protocol.Notification, []Metadata) {
channel, ok := sto.store[chanId]
if !ok {
return nil, nil, nil
}
n := len(channel.notifications)
res := make([]protocol.Notification, n)
meta := make([]Metadata, n)
copy(res, channel.notifications)
copy(meta, channel.meta)
return channel, res, meta
}
func (sto *InMemoryPendingStore) GetChannelUnfiltered(chanId InternalChannelId) (int64, []protocol.Notification, []Metadata, error) {
sto.lock.Lock()
defer sto.lock.Unlock()
channel, res, meta := sto.getChannelUnfiltered(chanId)
if channel == nil {
return 0, nil, nil, nil
}
return channel.topLevel, res, meta, nil
}
func (sto *InMemoryPendingStore) GetChannelSnapshot(chanId InternalChannelId) (int64, []protocol.Notification, error) {
topLevel, res, meta, _ := sto.GetChannelUnfiltered(chanId)
if res == nil {
return 0, nil, nil
}
res = FilterOutObsolete(res, meta)
return topLevel, res, nil
}
func (sto *InMemoryPendingStore) Scrub(chanId InternalChannelId, criteria ...string) error {
appId := ""
replaceTag := ""
switch len(criteria) {
case 2:
replaceTag = criteria[1]
fallthrough
case 1:
appId = criteria[0]
case 0:
default:
panic("Scrub() expects only up to two criterias")
}
sto.lock.Lock()
defer sto.lock.Unlock()
channel, res, meta := sto.getChannelUnfiltered(chanId)
if channel == nil {
return nil
}
fresh := FilterOutObsolete(res, meta)
res = make([]protocol.Notification, 0, len(fresh))
resMeta := make([]Metadata, 0, len(fresh))
i := 0
for j := range meta {
if meta[j].Obsolete {
continue
}
notif := fresh[i]
i++
if replaceTag != "" {
if notif.AppId == appId && meta[j].ReplaceTag == replaceTag {
continue
}
} else if notif.AppId == appId {
continue
}
res = append(res, notif)
resMeta = append(resMeta, meta[j])
}
// store as well
channel.notifications = res
channel.meta = resMeta
return nil
}
func (sto *InMemoryPendingStore) Close() {
// ignored
}
func (sto *InMemoryPendingStore) DropByMsgId(chanId InternalChannelId, targets []protocol.Notification) error {
sto.lock.Lock()
defer sto.lock.Unlock()
channel, ok := sto.store[chanId]
if !ok {
return nil
}
metaById := make(map[string]Metadata, len(channel.notifications))
for i, notif := range channel.notifications {
metaById[notif.MsgId] = channel.meta[i]
}
channel.notifications = FilterOutByMsgId(channel.notifications, targets)
resMeta := make([]Metadata, len(channel.notifications))
for i, notif := range channel.notifications {
resMeta[i] = metaById[notif.MsgId]
}
channel.meta = resMeta
return nil
}
// sanity check we implement the interface
var _ PendingStore = (*InMemoryPendingStore)(nil)
ubuntu-push-0.68+16.04.20160310.2/server/bootlog.go 0000644 0000156 0000165 00000002123 12670364255 021755 0 ustar pbuser pbgroup 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.68+16.04.20160310.2/server/runner_devices.go 0000644 0000156 0000165 00000005523 12670364255 023332 0 ustar pbuser pbgroup 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"
"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"`
TLSParsedConfig
}
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()
}
// 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, resource listener.SessionResourceManager, 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, resource, logger)
if err != nil {
BootLogFatalf("accepting device connections: %v", err)
}
}
}
ubuntu-push-0.68+16.04.20160310.2/server/dev/ 0000755 0000156 0000165 00000000000 12670364532 020537 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/server/dev/server.go 0000644 0000156 0000165 00000006513 12670364255 022403 0 ustar pbuser pbgroup 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/listener"
"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"`
// max notifications per application
MaxNotificationsPerApplication int `json:"max_notifications_per_app"`
}
type Storage struct {
sto store.PendingStore
maxNotificationsPerApplication int
}
func (storage *Storage) StoreForRequest(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
return storage.sto, nil
}
func (storage *Storage) GetMaxNotificationsPerApplication() int {
return storage.maxNotificationsPerApplication
}
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.LoadPEMs(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
storage := &Storage{
sto: sto,
maxNotificationsPerApplication: cfg.MaxNotificationsPerApplication,
}
lst, err := net.Listen("tcp", cfg.Addr())
if err != nil {
server.BootLogFatalf("start device listening: %v", err)
}
mux := api.MakeHandlersMux(storage, 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, nil)()
// listen for device connections
resource := &listener.NopSessionResourceManager{}
server.DevicesRunner(lst, func(conn net.Conn) error {
track := session.NewTracker(logger)
return session.Session(conn, broker, cfg, track)
}, logger, resource, &cfg.DevicesParsedConfig)()
}
ubuntu-push-0.68+16.04.20160310.2/server/runner_http.go 0000644 0000156 0000165 00000003661 12670364255 022670 0 ustar pbuser pbgroup 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 (
"crypto/tls"
"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.
// If tlsCfg is not nit server over TLS with the config.
func HTTPServeRunner(httpLst net.Listener, h http.Handler, parsedCfg *HTTPServeParsedConfig, tlsCfg *tls.Config) 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(),
}
if tlsCfg != nil {
httpLst = tls.NewListener(httpLst, tlsCfg)
}
return func() {
err := srv.Serve(httpLst)
if err != nil {
BootLogFatalf("accepting http connections: %v", err)
}
}
}
ubuntu-push-0.68+16.04.20160310.2/server/bootlog_test.go 0000644 0000156 0000165 00000002371 12670364255 023021 0 ustar pbuser pbgroup 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.68+16.04.20160310.2/launch_helper/ 0000755 0000156 0000165 00000000000 12670364532 021264 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/launch_helper/helper_output.go 0000644 0000156 0000165 00000011242 12670364255 024514 0 ustar pbuser pbgroup 0000000 0000000 /*
Copyright 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 launch_helper
import (
"encoding/json"
"time"
"launchpad.net/ubuntu-push/click"
)
// a Card is the usual “visual†presentation of a notification, used
// for bubbles and the notification centre (neé messaging menu)
type Card struct {
Summary string `json:"summary"` // required for the card to be presented
Body string `json:"body"` // defaults to empty
Actions []string `json:"actions"` // if empty (default), bubble is non-clickable. More entries change it to be clickable and (for bubbles) snap-decisions.
Icon string `json:"icon"` // an icon relating to the event being notified. Defaults to empty (no icon); a secondary icon relating to the application will be shown as well, irrespectively.
RawTimestamp int `json:"timestamp"` // seconds since epoch, only used for persist (for now). Timestamp() returns this if non-zero, current timestamp otherwise.
Persist bool `json:"persist"` // whether to show in notification centre; defaults to false
Popup bool `json:"popup"` // whether to show in a bubble. Users can disable this, and can easily miss them, so don't rely on it exclusively. Defaults to false.
}
// an EmblemCounter puts a number on an emblem on an app's icon in the launcher
type EmblemCounter struct {
Count int32 `json:"count"` // the number to show on the emblem counter
Visible bool `json:"visible"` // whether to show the emblem counter
}
// a Vibration generates a vibration in the form of a Pattern set in
// duration a pattern of on off states, repeated a number of times
type Vibration struct {
Pattern []uint32 `json:"pattern"`
Repeat uint32 `json:"repeat"` // defaults to 1. A value of zero is ignored (so it's like 1).
}
// a Notification can be any of the above
type Notification struct {
Card *Card `json:"card"` // defaults to nil (no card)
RawSound json.RawMessage `json:"sound"` // a boolean, or the relative path to a sound file. Users can disable this, so don't rely on it exclusively. Defaults to empty (no sound).
RawVibration json.RawMessage `json:"vibrate"` // users can disable this, blah blah. Can be Vibration, or boolean. Defaults to null (no vibration)
EmblemCounter *EmblemCounter `json:"emblem-counter"` // puts a counter on an emblem in the launcher. Defaults to nil (no change to emblem counter).
Tag string `json:"tag,omitempty"` // tag used for Clear/ListPersistent.
}
// HelperOutput is the expected output of a helper
type HelperOutput struct {
Message json.RawMessage `json:"message,omitempty"` // what to put in the post office's queue
Notification *Notification `json:"notification,omitempty"` // what to present to the user
}
// HelperResult is the result of a helper run for a particular app id
type HelperResult struct {
HelperOutput
Input *HelperInput
}
// HelperInput is what's passed in to a helper for it to work
type HelperInput struct {
kind string
App *click.AppId
NotificationId string
Payload json.RawMessage
}
// Timestamp() returns RawTimestamp if non-zero. If it's zero, returns
// the current time as second since epoch.
func (card *Card) Timestamp() int64 {
if card.RawTimestamp == 0 {
return time.Now().Unix()
} else {
return int64(card.RawTimestamp)
}
}
func (notification *Notification) Vibration(fallback *Vibration) *Vibration {
var b bool
var vib *Vibration
if notification.RawVibration == nil {
return nil
}
if json.Unmarshal(notification.RawVibration, &b) == nil {
if !b {
return nil
} else {
return fallback
}
}
if json.Unmarshal(notification.RawVibration, &vib) != nil {
return nil
}
if len(vib.Pattern) == 0 {
return nil
}
return vib
}
func (notification *Notification) Sound(fallback string) string {
var b bool
var s string
if notification.RawSound == nil {
return ""
}
if json.Unmarshal(notification.RawSound, &b) == nil {
if !b {
return ""
} else {
return fallback
}
}
if json.Unmarshal(notification.RawSound, &s) != nil {
return ""
}
return s
}
ubuntu-push-0.68+16.04.20160310.2/launch_helper/iface.go 0000644 0000156 0000165 00000001416 12670364255 022666 0 ustar pbuser pbgroup 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 launch_helper
type HelperPool interface {
Run(kind string, input *HelperInput)
Start() chan *HelperResult
Stop()
}
var InputBufferSize = 10
ubuntu-push-0.68+16.04.20160310.2/launch_helper/kindpool.go 0000644 0000156 0000165 00000022120 12670364255 023431 0 ustar pbuser pbgroup 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 launch_helper
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"sync"
"time"
xdg "launchpad.net/go-xdg/v0"
"launchpad.net/ubuntu-push/click"
"launchpad.net/ubuntu-push/launch_helper/cual"
"launchpad.net/ubuntu-push/launch_helper/legacy"
"launchpad.net/ubuntu-push/logger"
)
var (
ErrCantFindHelper = errors.New("can't find helper")
ErrCantFindLauncher = errors.New("can't find launcher for helper")
)
type HelperArgs struct {
Input *HelperInput
AppId string
FileIn string
FileOut string
Timer *time.Timer
ForcedStop bool
}
type HelperLauncher interface {
HelperInfo(app *click.AppId) (string, string)
InstallObserver(done func(string)) error
RemoveObserver() error
Launch(appId string, exec string, f1 string, f2 string) (string, error)
Stop(appId string, instanceId string) error
}
type kindHelperPool struct {
log logger.Logger
chOut chan *HelperResult
chIn chan *HelperInput
chDone chan *click.AppId
chStopped chan struct{}
launchers map[string]HelperLauncher
lock sync.Mutex
hmap map[string]*HelperArgs
maxRuntime time.Duration
maxNum int
// hook
growBacklog func([]*HelperInput, *HelperInput) []*HelperInput
}
// DefaultLaunchers produces the default map for kind -> HelperLauncher
func DefaultLaunchers(log logger.Logger) map[string]HelperLauncher {
return map[string]HelperLauncher{
"click": cual.New(log),
"legacy": legacy.New(log),
}
}
// a HelperPool that delegates to different per kind HelperLaunchers
func NewHelperPool(launchers map[string]HelperLauncher, log logger.Logger) HelperPool {
newPool := &kindHelperPool{
log: log,
hmap: make(map[string]*HelperArgs),
launchers: launchers,
maxRuntime: 5 * time.Second,
maxNum: 5,
}
newPool.growBacklog = newPool.doGrowBacklog
return newPool
}
func (pool *kindHelperPool) Start() chan *HelperResult {
pool.chOut = make(chan *HelperResult)
pool.chIn = make(chan *HelperInput, InputBufferSize)
pool.chDone = make(chan *click.AppId)
pool.chStopped = make(chan struct{})
for kind, launcher := range pool.launchers {
kind1 := kind
err := launcher.InstallObserver(func(iid string) {
pool.OneDone(kind1 + ":" + iid)
})
if err != nil {
panic(fmt.Errorf("failed to install helper observer for %s: %v", kind, err))
}
}
go pool.loop()
return pool.chOut
}
func (pool *kindHelperPool) loop() {
running := make(map[string]bool)
var backlog []*HelperInput
for {
select {
case in, ok := <-pool.chIn:
if !ok {
close(pool.chStopped)
return
}
if len(running) >= pool.maxNum || running[in.App.Original()] {
backlog = pool.growBacklog(backlog, in)
} else {
if pool.tryOne(in) {
running[in.App.Original()] = true
}
}
case app := <-pool.chDone:
delete(running, app.Original())
if len(backlog) == 0 {
continue
}
backlogSz := 0
done := false
for i, in := range backlog {
if in != nil {
if !done && !running[in.App.Original()] {
backlog[i] = nil
if pool.tryOne(in) {
running[in.App.Original()] = true
done = true
}
} else {
backlogSz++
}
}
}
backlog = pool.shrinkBacklog(backlog, backlogSz)
pool.log.Debugf("current helper input backlog has shrunk to %d entries.", backlogSz)
}
}
}
func (pool *kindHelperPool) doGrowBacklog(backlog []*HelperInput, in *HelperInput) []*HelperInput {
backlog = append(backlog, in)
pool.log.Debugf("current helper input backlog has grown to %d entries.", len(backlog))
return backlog
}
func (pool *kindHelperPool) shrinkBacklog(backlog []*HelperInput, backlogSz int) []*HelperInput {
if backlogSz == 0 {
return nil
}
if cap(backlog) < 2*backlogSz {
return backlog
}
pool.log.Debugf("copying backlog to avoid wasting too much space (%d/%d used)", backlogSz, cap(backlog))
clean := make([]*HelperInput, 0, backlogSz)
for _, bentry := range backlog {
if bentry != nil {
clean = append(clean, bentry)
}
}
return clean
}
func (pool *kindHelperPool) Stop() {
close(pool.chIn)
for kind, launcher := range pool.launchers {
err := launcher.RemoveObserver()
if err != nil {
panic(fmt.Errorf("failed to remove helper observer for &s: %v", kind, err))
}
}
// make Stop sync for tests
<-pool.chStopped
}
func (pool *kindHelperPool) Run(kind string, input *HelperInput) {
input.kind = kind
pool.chIn <- input
}
func (pool *kindHelperPool) tryOne(input *HelperInput) bool {
if pool.handleOne(input) != nil {
pool.failOne(input)
return false
}
return true
}
func (pool *kindHelperPool) failOne(input *HelperInput) {
pool.log.Errorf("unable to get helper output; putting payload into message")
pool.chOut <- &HelperResult{HelperOutput: HelperOutput{Message: input.Payload, Notification: nil}, Input: input}
}
func (pool *kindHelperPool) cleanupTempFiles(f1, f2 string) {
if f1 != "" {
os.Remove(f1)
}
if f2 != "" {
os.Remove(f2)
}
}
func (pool *kindHelperPool) handleOne(input *HelperInput) error {
launcher, ok := pool.launchers[input.kind]
if !ok {
pool.log.Errorf("unable to find launcher for kind: %v", input.kind)
return ErrCantFindLauncher
}
helperAppId, helperExec := launcher.HelperInfo(input.App)
if helperAppId == "" && helperExec == "" {
pool.log.Errorf("can't locate helper for app")
return ErrCantFindHelper
}
pool.log.Debugf("using helper %s (exec: %s) for app %s", helperAppId, helperExec, input.App)
var f1, f2 string
f1, err := pool.createInputTempFile(input)
defer func() {
if err != nil {
pool.cleanupTempFiles(f1, f2)
}
}()
if err != nil {
pool.log.Errorf("unable to create input tempfile: %v", err)
return err
}
f2, err = pool.createOutputTempFile(input)
if err != nil {
pool.log.Errorf("unable to create output tempfile: %v", err)
return err
}
args := HelperArgs{
AppId: helperAppId,
Input: input,
FileIn: f1,
FileOut: f2,
}
pool.lock.Lock()
defer pool.lock.Unlock()
iid, err := launcher.Launch(helperAppId, helperExec, f1, f2)
if err != nil {
pool.log.Errorf("unable to launch helper %s: %v", helperAppId, err)
return err
}
uid := input.kind + ":" + iid // unique across launchers
args.Timer = time.AfterFunc(pool.maxRuntime, func() {
pool.peekId(uid, func(a *HelperArgs) {
a.ForcedStop = true
err := launcher.Stop(helperAppId, iid)
if err != nil {
pool.log.Errorf("unable to forcefully stop helper %s: %v", helperAppId, err)
}
})
})
pool.hmap[uid] = &args
return nil
}
func (pool *kindHelperPool) peekId(uid string, cb func(*HelperArgs)) *HelperArgs {
pool.lock.Lock()
defer pool.lock.Unlock()
args, ok := pool.hmap[uid]
if ok {
cb(args)
return args
}
return nil
}
func (pool *kindHelperPool) OneDone(uid string) {
args := pool.peekId(uid, func(a *HelperArgs) {
a.Timer.Stop()
// dealt with, remove it
delete(pool.hmap, uid)
})
if args == nil {
// nothing to do
return
}
pool.chDone <- args.Input.App
defer func() {
pool.cleanupTempFiles(args.FileIn, args.FileOut)
}()
if args.ForcedStop {
pool.failOne(args.Input)
return
}
payload, err := ioutil.ReadFile(args.FileOut)
if err != nil {
pool.log.Errorf("unable to read output from %v helper: %v", args.AppId, err)
} else {
pool.log.Infof("%v helper output: %s", args.AppId, payload)
res := &HelperResult{Input: args.Input}
err = json.Unmarshal(payload, &res.HelperOutput)
if err != nil {
pool.log.Errorf("failed to parse HelperOutput from %v helper output: %v", args.AppId, err)
} else {
pool.chOut <- res
}
}
if err != nil {
pool.failOne(args.Input)
}
}
func (pool *kindHelperPool) createInputTempFile(input *HelperInput) (string, error) {
f1, err := getTempFilename(input.App.Package)
if err != nil {
return "", err
}
return f1, ioutil.WriteFile(f1, input.Payload, os.ModeTemporary)
}
func (pool *kindHelperPool) createOutputTempFile(input *HelperInput) (string, error) {
return getTempFilename(input.App.Package)
}
// helper helpers:
var xdgCacheHome = xdg.Cache.Home
func _getTempDir(pkgName string) (string, error) {
tmpDir := path.Join(xdgCacheHome(), pkgName)
err := os.MkdirAll(tmpDir, 0700)
return tmpDir, err
}
// override GetTempDir for testing without writing to ~/.cache/
var GetTempDir func(pkgName string) (string, error) = _getTempDir
func _getTempFilename(pkgName string) (string, error) {
tmpDir, err := GetTempDir(pkgName)
if err != nil {
return "", err
}
file, err := ioutil.TempFile(tmpDir, "push-helper")
if err != nil {
return "", err
}
defer file.Close()
return file.Name(), nil
}
var getTempFilename = _getTempFilename
ubuntu-push-0.68+16.04.20160310.2/launch_helper/cual/ 0000755 0000156 0000165 00000000000 12670364532 022210 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/launch_helper/cual/cual.go 0000644 0000156 0000165 00000004746 12670364255 023500 0 ustar pbuser pbgroup 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 cual
/*
#cgo pkg-config: ubuntu-app-launch-2
#include
gboolean add_observer (gpointer);
gboolean remove_observer (gpointer);
char* launch(gchar* app_id, gchar* exec, gchar* f1, gchar* f2, gpointer p);
gboolean stop(gchar* app_id, gchar* iid);
*/
import "C"
import (
"errors"
"unsafe"
"launchpad.net/ubuntu-push/click"
"launchpad.net/ubuntu-push/launch_helper/helper_finder"
"launchpad.net/ubuntu-push/logger"
)
func gstring(s string) *C.gchar {
return (*C.gchar)(C.CString(s))
}
type helperState struct {
log logger.Logger
done func(string)
}
//export helperDone
func helperDone(gp unsafe.Pointer, ciid *C.char) {
hs := (*helperState)(gp)
iid := C.GoString(ciid)
hs.done(iid)
}
var (
ErrCantObserve = errors.New("can't add observer")
ErrCantUnobserve = errors.New("can't remove observer")
ErrCantLaunch = errors.New("can't launch helper")
ErrCantStop = errors.New("can't stop helper")
)
func New(log logger.Logger) *helperState {
return &helperState{log: log}
}
func (hs *helperState) InstallObserver(done func(string)) error {
hs.done = done
if C.add_observer(C.gpointer(hs)) != C.TRUE {
return ErrCantObserve
}
return nil
}
func (hs *helperState) RemoveObserver() error {
if C.remove_observer(C.gpointer(hs)) != C.TRUE {
return ErrCantUnobserve
}
return nil
}
func (hs *helperState) HelperInfo(app *click.AppId) (string, string) {
return helper_finder.Helper(app, hs.log)
}
func (hs *helperState) Launch(appId, exec, f1, f2 string) (string, error) {
// launch(...) takes over ownership of things passed in
iid := C.GoString(C.launch(gstring(appId), gstring(exec), gstring(f1), gstring(f2), C.gpointer(hs)))
if iid == "" {
return "", ErrCantLaunch
}
return iid, nil
}
func (hs *helperState) Stop(appId, instanceId string) error {
if C.stop(gstring(appId), gstring(instanceId)) != C.TRUE {
return ErrCantStop
}
return nil
}
ubuntu-push-0.68+16.04.20160310.2/launch_helper/cual/cual_c.go 0000644 0000156 0000165 00000003461 12670364255 023773 0 ustar pbuser pbgroup 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 cual
// this is a .go to work around limitations in dh-golang
/*
#include
#include
#define HELPER_ERROR g_quark_from_static_string ("cgo-ual-helper-error-quark")
void helperDone(gpointer gp, const gchar * ciid);
static void observer_of_stop (const gchar * app_id, const gchar * instance_id, const gchar * helper_type, gpointer user_data) {
helperDone (user_data, instance_id);
}
char* launch(gchar* app_id, gchar* exec, gchar* f1, gchar* f2, gpointer p) {
const gchar* uris[4] = {exec, f1, f2, NULL};
gchar* iid = ubuntu_app_launch_start_multiple_helper ("push-helper", app_id, uris);
g_free (app_id);
g_free (exec);
g_free (f1);
g_free (f2);
return iid;
}
gboolean add_observer(gpointer p) {
return ubuntu_app_launch_observer_add_helper_stop(observer_of_stop, "push-helper", p);
}
gboolean remove_observer(gpointer p) {
return ubuntu_app_launch_observer_delete_helper_stop(observer_of_stop, "push-helper", p);
}
gboolean stop(gchar* app_id, gchar* iid) {
gboolean res;
res = ubuntu_app_launch_stop_multiple_helper ("push-helper", app_id, iid);
g_free (app_id);
g_free (iid);
return res;
}
*/
import "C"
ubuntu-push-0.68+16.04.20160310.2/launch_helper/kindpool_test.go 0000644 0000156 0000165 00000041726 12670364255 024505 0 ustar pbuser pbgroup 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 launch_helper
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
xdg "launchpad.net/go-xdg/v0"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/click"
clickhelp "launchpad.net/ubuntu-push/click/testing"
"launchpad.net/ubuntu-push/launch_helper/cual"
helpers "launchpad.net/ubuntu-push/testing"
)
type poolSuite struct {
log *helpers.TestLogger
pool HelperPool
fakeLauncher *fakeHelperLauncher
}
var _ = Suite(&poolSuite{})
func takeNext(ch chan *HelperResult, c *C) *HelperResult {
select {
case res := <-ch:
return res
case <-time.After(time.Second):
c.Fatal("timeout waiting for result")
}
return nil
}
type fakeHelperLauncher struct {
done func(string)
obs int
err error
lhex string
argCh chan [5]string
runid int
}
func (fhl *fakeHelperLauncher) InstallObserver(done func(string)) error {
fhl.done = done
fhl.obs++
return nil
}
func (fhl *fakeHelperLauncher) RemoveObserver() error {
fhl.obs--
return nil
}
func (fhl *fakeHelperLauncher) HelperInfo(app *click.AppId) (string, string) {
if app.Click {
return app.Base() + "-helper", "bar"
} else {
return "", fhl.lhex
}
}
func (fhl *fakeHelperLauncher) Launch(appId string, exec string, f1 string, f2 string) (string, error) {
fhl.argCh <- [5]string{"Launch", appId, exec, f1, f2}
runid := fmt.Sprintf("%d", fhl.runid)
fhl.runid++
return runid, fhl.err
}
func (fhl *fakeHelperLauncher) Stop(appId string, iid string) error {
fhl.argCh <- [5]string{"Stop", appId, iid, "", ""}
return nil
}
func (s *poolSuite) waitForArgs(c *C, method string) [5]string {
var args [5]string
select {
case args = <-s.fakeLauncher.argCh:
case <-time.After(2 * time.Second):
c.Fatal("didn't call " + method)
}
c.Assert(args[0], Equals, method)
return args
}
func (s *poolSuite) SetUpSuite(c *C) {
xdgCacheHome = c.MkDir
}
func (s *poolSuite) TearDownSuite(c *C) {
xdgCacheHome = xdg.Cache.Home
}
func (s *poolSuite) SetUpTest(c *C) {
s.log = helpers.NewTestLogger(c, "debug")
s.fakeLauncher = &fakeHelperLauncher{argCh: make(chan [5]string, 10)}
s.pool = NewHelperPool(map[string]HelperLauncher{"fake": s.fakeLauncher}, s.log)
}
func (s *poolSuite) TearDownTest(c *C) {
s.pool = nil
}
func (s *poolSuite) TestDefaultLaunchers(c *C) {
launchers := DefaultLaunchers(s.log)
_, ok := launchers["click"]
c.Check(ok, Equals, true)
_, ok = launchers["legacy"]
c.Check(ok, Equals, true)
}
// check that Stop (tries to) remove the observer
func (s *poolSuite) TestStartStopWork(c *C) {
c.Check(s.fakeLauncher.obs, Equals, 0)
s.pool.Start()
c.Check(s.fakeLauncher.done, NotNil)
c.Check(s.fakeLauncher.obs, Equals, 1)
s.pool.Stop()
c.Check(s.fakeLauncher.obs, Equals, 0)
}
func (s *poolSuite) TestRunLaunches(c *C) {
s.pool.Start()
defer s.pool.Stop()
appId := "com.example.test_test-app"
app := clickhelp.MustParseAppId(appId)
helpId := app.Base() + "-helper"
input := HelperInput{
App: app,
NotificationId: "foo",
Payload: []byte(`"hello"`),
}
s.pool.Run("fake", &input)
launchArgs := s.waitForArgs(c, "Launch")
c.Check(launchArgs[:3], DeepEquals, []string{"Launch", helpId, "bar"})
args := s.pool.(*kindHelperPool).peekId("fake:0", func(*HelperArgs) {})
c.Assert(args, NotNil)
args.Timer.Stop()
c.Check(args.AppId, Equals, helpId)
c.Check(args.Input, Equals, &input)
c.Check(args.FileIn, NotNil)
c.Check(args.FileOut, NotNil)
}
func (s *poolSuite) TestRunLaunchesLegacyStyle(c *C) {
s.fakeLauncher.lhex = "lhex"
s.pool.Start()
defer s.pool.Stop()
appId := "_legacy"
app := clickhelp.MustParseAppId(appId)
input := HelperInput{
App: app,
NotificationId: "foo",
Payload: []byte(`"hello"`),
}
s.pool.Run("fake", &input)
launchArgs := s.waitForArgs(c, "Launch")
c.Check(launchArgs[:3], DeepEquals, []string{"Launch", "", "lhex"})
args := s.pool.(*kindHelperPool).peekId("fake:0", func(*HelperArgs) {})
c.Assert(args, NotNil)
args.Timer.Stop()
c.Check(args.Input, Equals, &input)
c.Check(args.FileIn, NotNil)
c.Check(args.FileOut, NotNil)
}
func (s *poolSuite) TestGetOutputIfHelperLaunchFail(c *C) {
ch := s.pool.Start()
defer s.pool.Stop()
app := clickhelp.MustParseAppId("com.example.test_test-app")
input := HelperInput{
App: app,
NotificationId: "foo",
Payload: []byte(`"hello"`),
}
s.pool.Run("not-there", &input)
res := takeNext(ch, c)
c.Check(res.Message, DeepEquals, input.Payload)
c.Check(res.Notification, IsNil)
c.Check(*res.Input, DeepEquals, input)
}
func (s *poolSuite) TestGetOutputIfHelperLaunchFail2(c *C) {
ch := s.pool.Start()
defer s.pool.Stop()
app := clickhelp.MustParseAppId("_legacy")
input := HelperInput{
App: app,
NotificationId: "foo",
Payload: []byte(`"hello"`),
}
s.pool.Run("fake", &input)
res := takeNext(ch, c)
c.Check(res.Message, DeepEquals, input.Payload)
c.Check(res.Notification, IsNil)
c.Check(*res.Input, DeepEquals, input)
}
func (s *poolSuite) TestRunCantLaunch(c *C) {
s.fakeLauncher.err = cual.ErrCantLaunch
ch := s.pool.Start()
defer s.pool.Stop()
appId := "com.example.test_test-app"
app := clickhelp.MustParseAppId(appId)
helpId := app.Base() + "-helper"
input := HelperInput{
App: app,
NotificationId: "foo",
Payload: []byte(`"hello"`),
}
s.pool.Run("fake", &input)
launchArgs := s.waitForArgs(c, "Launch")
c.Check(launchArgs[:3], DeepEquals, []string{"Launch", helpId, "bar"})
res := takeNext(ch, c)
c.Check(res.Message, DeepEquals, input.Payload)
c.Check(s.log.Captured(), Equals, "DEBUG using helper com.example.test_test-app-helper (exec: bar) for app com.example.test_test-app\n"+"ERROR unable to launch helper com.example.test_test-app-helper: can't launch helper\n"+"ERROR unable to get helper output; putting payload into message\n")
}
func (s *poolSuite) TestRunLaunchesAndTimeout(c *C) {
s.pool.(*kindHelperPool).maxRuntime = 500 * time.Millisecond
ch := s.pool.Start()
defer s.pool.Stop()
appId := "com.example.test_test-app"
app := clickhelp.MustParseAppId(appId)
helpId := app.Base() + "-helper"
input := HelperInput{
App: app,
NotificationId: "foo",
Payload: []byte(`"hello"`),
}
s.pool.Run("fake", &input)
launchArgs := s.waitForArgs(c, "Launch")
c.Check(launchArgs[0], Equals, "Launch")
stopArgs := s.waitForArgs(c, "Stop")
c.Check(stopArgs[:3], DeepEquals, []string{"Stop", helpId, "0"})
// this will be invoked
go s.fakeLauncher.done("0")
res := takeNext(ch, c)
c.Check(res.Message, DeepEquals, input.Payload)
}
func (s *poolSuite) TestOneDoneNop(c *C) {
pool := s.pool.(*kindHelperPool)
pool.OneDone("")
}
func (s *poolSuite) TestOneDoneOnValid(c *C) {
pool := s.pool.(*kindHelperPool)
ch := pool.Start()
defer pool.Stop()
d := c.MkDir()
app := clickhelp.MustParseAppId("com.example.test_test-app")
input := &HelperInput{
App: app,
}
args := HelperArgs{
Input: input,
FileOut: filepath.Join(d, "file_out.json"),
Timer: time.NewTimer(0),
}
pool.hmap["l:1"] = &args
f, err := os.Create(args.FileOut)
c.Assert(err, IsNil)
defer f.Close()
_, err = f.Write([]byte(`{"notification": {"sound": "hello", "tag": "a-tag"}}`))
c.Assert(err, IsNil)
go pool.OneDone("l:1")
res := takeNext(ch, c)
expected := HelperOutput{Notification: &Notification{RawSound: json.RawMessage(`"hello"`), Tag: "a-tag"}}
c.Check(res.HelperOutput, DeepEquals, expected)
c.Check(pool.hmap, HasLen, 0)
}
func (s *poolSuite) TestOneDoneOnBadFileOut(c *C) {
pool := s.pool.(*kindHelperPool)
ch := pool.Start()
defer pool.Stop()
app := clickhelp.MustParseAppId("com.example.test_test-app")
args := HelperArgs{
Input: &HelperInput{
App: app,
NotificationId: "foo",
Payload: []byte(`"hello"`),
},
FileOut: "/does-not-exist",
Timer: time.NewTimer(0),
}
pool.hmap["l:1"] = &args
go pool.OneDone("l:1")
res := takeNext(ch, c)
expected := HelperOutput{Message: args.Input.Payload}
c.Check(res.HelperOutput, DeepEquals, expected)
}
func (s *poolSuite) TestOneDonwOnBadJSONOut(c *C) {
pool := s.pool.(*kindHelperPool)
ch := pool.Start()
defer pool.Stop()
d := c.MkDir()
app := clickhelp.MustParseAppId("com.example.test_test-app")
args := HelperArgs{
FileOut: filepath.Join(d, "file_out.json"),
Input: &HelperInput{
App: app,
NotificationId: "foo",
Payload: []byte(`"hello"`),
},
Timer: time.NewTimer(0),
}
pool.hmap["l:1"] = &args
f, err := os.Create(args.FileOut)
c.Assert(err, IsNil)
defer f.Close()
_, err = f.Write([]byte(`potato`))
c.Assert(err, IsNil)
go pool.OneDone("l:1")
res := takeNext(ch, c)
expected := HelperOutput{Message: args.Input.Payload}
c.Check(res.HelperOutput, DeepEquals, expected)
}
func (s *poolSuite) TestCreateInputTempFile(c *C) {
tmpDir := c.MkDir()
GetTempDir = func(pkgName string) (string, error) {
return tmpDir, nil
}
// restore it when we are done
defer func() {
GetTempDir = _getTempDir
}()
app := clickhelp.MustParseAppId("com.example.test_test-app")
input := &HelperInput{
App: app,
NotificationId: "foo",
Payload: []byte(`"hello"`),
}
pool := s.pool.(*kindHelperPool)
f1, err := pool.createInputTempFile(input)
c.Assert(err, IsNil)
c.Check(f1, Not(Equals), "")
f2, err := pool.createOutputTempFile(input)
c.Assert(err, IsNil)
c.Check(f2, Not(Equals), "")
files, err := ioutil.ReadDir(filepath.Dir(f1))
c.Check(err, IsNil)
c.Check(files, HasLen, 2)
}
func (s *poolSuite) TestGetTempFilename(c *C) {
tmpDir := c.MkDir()
GetTempDir = func(pkgName string) (string, error) {
return tmpDir, nil
}
// restore it when we are done
defer func() {
GetTempDir = _getTempDir
}()
fname, err := getTempFilename("pkg.name")
c.Check(err, IsNil)
dirname := filepath.Dir(fname)
files, err := ioutil.ReadDir(dirname)
c.Check(err, IsNil)
c.Check(files, HasLen, 1)
}
func (s *poolSuite) TestGetTempDir(c *C) {
tmpDir := c.MkDir()
oldCacheHome := xdgCacheHome
xdgCacheHome = func() string {
return tmpDir
}
// restore it when we are done
defer func() {
xdgCacheHome = oldCacheHome
}()
dname, err := GetTempDir("pkg.name")
c.Check(err, IsNil)
c.Check(dname, Equals, filepath.Join(tmpDir, "pkg.name"))
}
// checks that the a second helper run of an already-running helper
// (for an app) goes to the backlog
func (s *poolSuite) TestSecondRunSameAppToBacklog(c *C) {
ch := s.pool.Start()
defer s.pool.Stop()
app1 := clickhelp.MustParseAppId("com.example.test_test-app-1")
input1 := &HelperInput{
App: app1,
NotificationId: "foo1",
Payload: []byte(`"hello1"`),
}
app2 := clickhelp.MustParseAppId("com.example.test_test-app-1")
input2 := &HelperInput{
App: app2,
NotificationId: "foo2",
Payload: []byte(`"hello2"`),
}
c.Assert(app1.Base(), Equals, app2.Base())
s.pool.Run("fake", input1)
s.pool.Run("fake", input2)
s.waitForArgs(c, "Launch")
go s.fakeLauncher.done("0")
takeNext(ch, c)
// this is where we check that:
c.Check(s.log.Captured(), Matches, `(?ms).* helper input backlog has grown to 1 entries.$`)
}
// checks that the an Nth helper run goes to the backlog
func (s *poolSuite) TestRunNthAppToBacklog(c *C) {
s.pool.(*kindHelperPool).maxNum = 2
doGrowBacklog := s.pool.(*kindHelperPool).doGrowBacklog
grownTo1 := make(chan struct{})
s.pool.(*kindHelperPool).growBacklog = func(bl []*HelperInput, in *HelperInput) []*HelperInput {
res := doGrowBacklog(bl, in)
if len(res) == 1 {
close(grownTo1)
}
return res
}
ch := s.pool.Start()
defer s.pool.Stop()
app1 := clickhelp.MustParseAppId("com.example.test_test-app-1")
input1 := &HelperInput{
App: app1,
NotificationId: "foo1",
Payload: []byte(`"hello1"`),
}
app2 := clickhelp.MustParseAppId("com.example.test_test-app-2")
input2 := &HelperInput{
App: app2,
NotificationId: "foo2",
Payload: []byte(`"hello2"`),
}
app3 := clickhelp.MustParseAppId("com.example.test_test-app-3")
input3 := &HelperInput{
App: app3,
NotificationId: "foo3",
Payload: []byte(`"hello3"`),
}
s.pool.Run("fake", input1)
s.waitForArgs(c, "Launch")
s.pool.Run("fake", input2)
s.log.ResetCapture()
s.waitForArgs(c, "Launch")
s.pool.Run("fake", input3)
select {
case <-grownTo1:
case <-time.After(time.Second):
c.Fatal("timeout waiting for result")
}
go s.fakeLauncher.done("0")
s.waitForArgs(c, "Launch")
res := takeNext(ch, c)
c.Assert(res, NotNil)
c.Assert(res.Input, NotNil)
c.Assert(res.Input.App, NotNil)
c.Assert(res.Input.App.Original(), Equals, "com.example.test_test-app-1")
go s.fakeLauncher.done("1")
go s.fakeLauncher.done("2")
takeNext(ch, c)
takeNext(ch, c)
// this is the crux: we're checking that the third Run() went to the backlog.
c.Check(s.log.Captured(), Matches,
`(?ms).* helper input backlog has grown to 1 entries\.$.*shrunk to 0 entries\.$`)
}
func (s *poolSuite) TestRunBacklogFailedContinuesDiffApp(c *C) {
s.pool.(*kindHelperPool).maxNum = 1
doGrowBacklog := s.pool.(*kindHelperPool).doGrowBacklog
grownTo3 := make(chan struct{})
s.pool.(*kindHelperPool).growBacklog = func(bl []*HelperInput, in *HelperInput) []*HelperInput {
res := doGrowBacklog(bl, in)
if len(res) == 3 {
close(grownTo3)
}
return res
}
ch := s.pool.Start()
defer s.pool.Stop()
app1 := clickhelp.MustParseAppId("com.example.test_test-app-1")
input1 := &HelperInput{
App: app1,
NotificationId: "foo1",
Payload: []byte(`"hello1"`),
}
app2 := clickhelp.MustParseAppId("com.example.test_test-app-2")
input2 := &HelperInput{
App: app2,
NotificationId: "foo2",
Payload: []byte(`"hello2"`),
}
app3 := clickhelp.MustParseAppId("com.example.test_test-app-3")
input3 := &HelperInput{
App: app3,
NotificationId: "foo3",
Payload: []byte(`"hello3"`),
}
app4 := clickhelp.MustParseAppId("com.example.test_test-app-4")
input4 := &HelperInput{
App: app4,
NotificationId: "foo4",
Payload: []byte(`"hello4"`),
}
s.pool.Run("fake", input1)
s.waitForArgs(c, "Launch")
s.pool.Run("NOT-THERE", input2) // this will fail
s.pool.Run("fake", input3)
s.pool.Run("fake", input4)
select {
case <-grownTo3:
case <-time.After(time.Second):
c.Fatal("timeout waiting for result")
}
go s.fakeLauncher.done("0")
// Everything up to here was just set-up.
//
// What we're checking for is that, if a helper launch fails, the
// next one in the backlog is picked up.
takeNext(ch, c)
takeNext(ch, c)
s.waitForArgs(c, "Launch")
go s.fakeLauncher.done("1")
c.Assert(takeNext(ch, c).Input.App, Equals, app3)
c.Check(s.log.Captured(), Matches,
`(?ms).* helper input backlog has grown to 3 entries\.$.*shrunk to 1 entries\.$`)
}
func (s *poolSuite) TestBigBacklogShrinks(c *C) {
oldBufSz := InputBufferSize
InputBufferSize = 0
defer func() { InputBufferSize = oldBufSz }()
s.pool.(*kindHelperPool).maxNum = 1
ch := s.pool.Start()
defer s.pool.Stop()
app := clickhelp.MustParseAppId("com.example.test_test-app")
s.pool.Run("fake", &HelperInput{App: app, NotificationId: "0", Payload: []byte(`""`)})
s.pool.Run("fake", &HelperInput{App: app, NotificationId: "1", Payload: []byte(`""`)})
s.pool.Run("fake", &HelperInput{App: app, NotificationId: "2", Payload: []byte(`""`)})
s.waitForArgs(c, "Launch")
go s.fakeLauncher.done("0")
takeNext(ch, c)
// so now there's one done, one "running", and one more waiting.
// kicking it forward one more notch before checking the logs:
s.waitForArgs(c, "Launch")
go s.fakeLauncher.done("1")
takeNext(ch, c)
// (two done, one "running")
c.Check(s.log.Captured(), Matches, `(?ms).* shrunk to 1 entries\.$`)
// and the backlog shrinker shrunk the backlog
c.Check(s.log.Captured(), Matches, `(?ms).*copying backlog to avoid wasting too much space .*`)
}
func (s *poolSuite) TestBacklogShrinkerNilToNil(c *C) {
pool := s.pool.(*kindHelperPool)
c.Check(pool.shrinkBacklog(nil, 0), IsNil)
}
func (s *poolSuite) TestBacklogShrinkerEmptyToNil(c *C) {
pool := s.pool.(*kindHelperPool)
empty := []*HelperInput{nil, nil, nil}
c.Check(pool.shrinkBacklog(empty, 0), IsNil)
}
func (s *poolSuite) TestBacklogShrinkerFullUntouched(c *C) {
pool := s.pool.(*kindHelperPool)
input := &HelperInput{}
full := []*HelperInput{input, input, input}
c.Check(pool.shrinkBacklog(full, 3), DeepEquals, full)
}
func (s *poolSuite) TestBacklogShrinkerSparseShrunk(c *C) {
pool := s.pool.(*kindHelperPool)
input := &HelperInput{}
sparse := []*HelperInput{nil, input, nil, input, nil}
full := []*HelperInput{input, input}
c.Check(pool.shrinkBacklog(sparse, 2), DeepEquals, full)
}
ubuntu-push-0.68+16.04.20160310.2/launch_helper/helper.go 0000644 0000156 0000165 00000003362 12670364255 023100 0 ustar pbuser pbgroup 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 .
*/
// launch_helper wraps ubuntu_app_launch to enable using application
// helpers. The useful part is HelperRunner
package launch_helper
import (
"encoding/json"
"launchpad.net/ubuntu-push/logger"
)
type trivialHelperLauncher struct {
log logger.Logger
chOut chan *HelperResult
chIn chan *HelperInput
}
// a trivial HelperPool that doesn't launch anything at all
func NewTrivialHelperPool(log logger.Logger) HelperPool {
return &trivialHelperLauncher{log: log}
}
func (triv *trivialHelperLauncher) Start() chan *HelperResult {
triv.chOut = make(chan *HelperResult)
triv.chIn = make(chan *HelperInput, InputBufferSize)
go func() {
for i := range triv.chIn {
res := &HelperResult{Input: i}
err := json.Unmarshal(i.Payload, &res.HelperOutput)
if err != nil {
triv.log.Debugf("failed to parse HelperOutput from message, leaving it alone: %v", err)
res.Message = i.Payload
res.Notification = nil
}
triv.chOut <- res
}
}()
return triv.chOut
}
func (triv *trivialHelperLauncher) Stop() {
close(triv.chIn)
}
func (triv *trivialHelperLauncher) Run(kind string, input *HelperInput) {
triv.chIn <- input
}
ubuntu-push-0.68+16.04.20160310.2/launch_helper/helper_finder/ 0000755 0000156 0000165 00000000000 12670364532 024072 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/launch_helper/helper_finder/helper_finder_test.go 0000644 0000156 0000165 00000014655 12670364255 030303 0 ustar pbuser pbgroup 0000000 0000000 package helper_finder
import (
"os"
"path/filepath"
"testing"
"time"
. "launchpad.net/gocheck"
helpers "launchpad.net/ubuntu-push/testing"
"launchpad.net/ubuntu-push/click"
)
type helperSuite struct {
oldHookPath string
symlinkPath string
oldHelpersDataPath string
log *helpers.TestLogger
}
func TestHelperFinder(t *testing.T) { TestingT(t) }
var _ = Suite(&helperSuite{})
func (s *helperSuite) SetUpTest(c *C) {
s.oldHookPath = hookPath
hookPath = c.MkDir()
s.symlinkPath = c.MkDir()
s.oldHelpersDataPath = helpersDataPath
helpersDataPath = filepath.Join(c.MkDir(), "helpers_data.json")
s.log = helpers.NewTestLogger(c, "debug")
}
func (s *helperSuite) createHookfile(name string, content string) error {
symlink := filepath.Join(hookPath, name) + ".json"
filename := filepath.Join(s.symlinkPath, name)
f, err := os.Create(filename)
if err != nil {
return err
}
_, err = f.WriteString(content)
if err != nil {
return err
}
err = os.Symlink(filename, symlink)
if err != nil {
return err
}
return nil
}
func (s *helperSuite) createHelpersDatafile(content string) error {
f, err := os.Create(helpersDataPath)
if err != nil {
return err
}
_, err = f.WriteString(content)
if err != nil {
return err
}
return nil
}
func (s *helperSuite) TearDownTest(c *C) {
hookPath = s.oldHookPath
os.Remove(helpersDataPath)
helpersDataPath = s.oldHelpersDataPath
helpersDataMtime = time.Now().Add(-1 * time.Hour)
helpersInfo = nil
}
func (s *helperSuite) TestHelperBasic(c *C) {
c.Assert(s.createHelpersDatafile(`{"com.example.test": {"helper_id": "com.example.test_test-helper_1", "exec": "tsthlpr"}}`), IsNil)
app, err := click.ParseAppId("com.example.test_test-app_1")
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "com.example.test_test-helper_1")
c.Check(hex, Equals, "tsthlpr")
}
func (s *helperSuite) TestHelperFindsSpecific(c *C) {
fileContent := `{"com.example.test_test-other-app": {"exec": "aaaaaaa", "helper_id": "com.example.test_aaaa-helper_1"},
"com.example.test_test-app": {"exec": "tsthlpr", "helper_id": "com.example.test_test-helper_1"}}`
c.Assert(s.createHelpersDatafile(fileContent), IsNil)
app, err := click.ParseAppId("com.example.test_test-app_1")
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "com.example.test_test-helper_1")
c.Check(hex, Equals, "tsthlpr")
}
func (s *helperSuite) TestHelperCanFail(c *C) {
fileContent := `{"com.example.test_test-other-app": {"exec": "aaaaaaa", "helper_id": "com.example.test_aaaa-helper_1"}}`
c.Assert(s.createHelpersDatafile(fileContent), IsNil)
app, err := click.ParseAppId("com.example.test_test-app_1")
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "")
c.Check(hex, Equals, "")
}
func (s *helperSuite) TestHelperFailInvalidJson(c *C) {
fileContent := `{invalid json"com.example.test_test-other-app": {"exec": "aaaaaaa", "helper_id": "com.example.test_aaaa-helper_1"}}`
c.Assert(s.createHelpersDatafile(fileContent), IsNil)
app, err := click.ParseAppId("com.example.test_test-app_1")
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "")
c.Check(hex, Equals, "")
}
func (s *helperSuite) TestHelperFailMissingExec(c *C) {
fileContent := `{"com.example.test_test-app": {"helper_id": "com.example.test_aaaa-helper_1"}}`
c.Assert(s.createHelpersDatafile(fileContent), IsNil)
app, err := click.ParseAppId("com.example.test_test-app_1")
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "")
c.Check(hex, Equals, "")
}
func (s *helperSuite) TestHelperlegacy(c *C) {
appname := "ubuntu-system-settings"
app, err := click.ParseAppId("_" + appname)
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "")
c.Check(hex, Equals, "")
}
// Missing Cache file test
func (s *helperSuite) TestHelperMissingCacheFile(c *C) {
c.Assert(s.createHookfile("com.example.test_test-helper_1", `{"exec": "tsthlpr"}`), IsNil)
app, err := click.ParseAppId("com.example.test_test-app_1")
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "com.example.test_test-helper_1")
c.Check(hex, Equals, filepath.Join(s.symlinkPath, "tsthlpr"))
c.Check(s.log.Captured(), Matches, ".*(?i)Cache file not found, falling back to .json file lookup\n")
}
func (s *helperSuite) TestHelperFromHookBasic(c *C) {
c.Assert(s.createHookfile("com.example.test_test-helper_1", `{"exec": "tsthlpr"}`), IsNil)
app, err := click.ParseAppId("com.example.test_test-app_1")
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "com.example.test_test-helper_1")
c.Check(hex, Equals, filepath.Join(s.symlinkPath, "tsthlpr"))
}
func (s *helperSuite) TestHelperFromHookFindsSpecific(c *C) {
// Glob() sorts, so the first one will come first
c.Assert(s.createHookfile("com.example.test_aaaa-helper_1", `{"exec": "aaaaaaa", "app_id": "com.example.test_test-other-app"}`), IsNil)
c.Assert(s.createHookfile("com.example.test_test-helper_1", `{"exec": "tsthlpr", "app_id": "com.example.test_test-app"}`), IsNil)
app, err := click.ParseAppId("com.example.test_test-app_1")
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "com.example.test_test-helper_1")
c.Check(hex, Equals, filepath.Join(s.symlinkPath, "tsthlpr"))
}
func (s *helperSuite) TestHelperFromHookCanFail(c *C) {
c.Assert(s.createHookfile("com.example.test_aaaa-helper_1", `{"exec": "aaaaaaa", "app_id": "com.example.test_test-other-app"}`), IsNil)
app, err := click.ParseAppId("com.example.test_test-app_1")
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "")
c.Check(hex, Equals, "")
}
func (s *helperSuite) TestHelperFromHookInvalidJson(c *C) {
c.Assert(s.createHookfile("com.example.test_aaaa-helper_1", `invalid json {"exec": "aaaaaaa", "app_id": "com.example.test_test-other-app"}`), IsNil)
app, err := click.ParseAppId("com.example.test_test-app_1")
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "")
c.Check(hex, Equals, "")
}
func (s *helperSuite) TestHelperFromHooFailBrokenSymlink(c *C) {
name := "com.example.test_aaaa-helper_1"
c.Assert(s.createHookfile(name, `{"exec": "aaaaaaa", "app_id": "com.example.test_test-other-app"}`), IsNil)
filename := filepath.Join(s.symlinkPath, name)
os.Remove(filename)
app, err := click.ParseAppId("com.example.test_test-app_1")
c.Assert(err, IsNil)
hid, hex := Helper(app, s.log)
c.Check(hid, Equals, "")
c.Check(hex, Equals, "")
}
ubuntu-push-0.68+16.04.20160310.2/launch_helper/helper_finder/helper_finder.go 0000644 0000156 0000165 00000005107 12670364255 027234 0 ustar pbuser pbgroup 0000000 0000000 package helper_finder
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"sync"
"time"
"launchpad.net/go-xdg/v0"
"launchpad.net/ubuntu-push/click"
"launchpad.net/ubuntu-push/logger"
)
type helperValue struct {
HelperId string `json:"helper_id"`
Exec string `json:"exec"`
}
type hookFile struct {
AppId string `json:"app_id"`
Exec string `json:"exec"`
}
var mapLock sync.Mutex
var helpersInfo = make(map[string]helperValue)
var helpersDataMtime time.Time
var helpersDataPath = filepath.Join(xdg.Data.Home(), "ubuntu-push-client", "helpers_data.json")
var hookPath = filepath.Join(xdg.Data.Home(), "ubuntu-push-client", "helpers")
var hookExt = ".json"
// helperFromHookfile figures out the app id and executable of the untrusted
// helper for this app.
func helperFromHookFile(app *click.AppId) (helperAppId string, helperExec string) {
matches, err := filepath.Glob(filepath.Join(hookPath, app.Package+"_*"+hookExt))
if err != nil {
return "", ""
}
var v hookFile
for _, m := range matches {
abs, err := filepath.EvalSymlinks(m)
if err != nil {
continue
}
data, err := ioutil.ReadFile(abs)
if err != nil {
continue
}
err = json.Unmarshal(data, &v)
if err != nil {
continue
}
if v.Exec != "" && (v.AppId == "" || v.AppId == app.Base()) {
basename := filepath.Base(m)
helperAppId = basename[:len(basename)-len(hookExt)]
helperExec = filepath.Join(filepath.Dir(abs), v.Exec)
return helperAppId, helperExec
}
}
return "", ""
}
// Helper figures out the id and executable of the untrusted
// helper for this app.
func Helper(app *click.AppId, log logger.Logger) (helperAppId string, helperExec string) {
if !app.Click {
return "", ""
}
fInfo, err := os.Stat(helpersDataPath)
if err != nil {
// cache file is missing, go via the slow route
log.Infof("cache file not found, falling back to .json file lookup")
return helperFromHookFile(app)
}
// get the lock as the map can be changed while we read
mapLock.Lock()
defer mapLock.Unlock()
if helpersInfo == nil || fInfo.ModTime().After(helpersDataMtime) {
data, err := ioutil.ReadFile(helpersDataPath)
if err != nil {
return "", ""
}
err = json.Unmarshal(data, &helpersInfo)
if err != nil {
return "", ""
}
helpersDataMtime = fInfo.ModTime()
}
var info helperValue
info, ok := helpersInfo[app.Base()]
if !ok {
// ok, appid wasn't there, try with the package
info, ok = helpersInfo[app.Package]
if !ok {
return "", ""
}
}
if info.Exec != "" {
helperAppId = info.HelperId
helperExec = info.Exec
return helperAppId, helperExec
}
return "", ""
}
ubuntu-push-0.68+16.04.20160310.2/launch_helper/helper_output_test.go 0000644 0000156 0000165 00000006115 12670364255 025556 0 ustar pbuser pbgroup 0000000 0000000 /*
Copyright 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 launch_helper
import (
"encoding/json"
"time"
. "launchpad.net/gocheck"
)
type outSuite struct{}
var _ = Suite(&outSuite{})
func (*outSuite) TestCardGetTimestamp(c *C) {
t := time.Now().Add(-2 * time.Second)
var card Card
err := json.Unmarshal([]byte(`{"timestamp": 12}`), &card)
c.Assert(err, IsNil)
c.Check(card, DeepEquals, Card{RawTimestamp: 12})
c.Check(time.Unix((&Card{}).Timestamp(), 0).After(t), Equals, true)
c.Check((&Card{RawTimestamp: 42}).Timestamp(), Equals, int64(42))
}
func (*outSuite) TestBadVibeBegetsNilVibe(c *C) {
fbck := &Vibration{Repeat: 2}
for _, s := range []string{
`{}`,
`{"vibrate": "foo"}`,
`{"vibrate": {}}`,
`{"vibrate": false}`, // not bad, but rather pointless
`{"vibrate": {"repeat": 2}}`, // no pattern
`{"vibrate": {"repeat": "foo"}}`,
`{"vibrate": {"pattern": "foo"}}`,
`{"vibrate": {"pattern": ["foo"]}}`,
`{"vibrate": {"pattern": null}}`,
`{"vibrate": {"pattern": [-1]}}`,
`{"vibrate": {"pattern": [1], "repeat": -1}}`,
} {
var notif *Notification
err := json.Unmarshal([]byte(s), ¬if)
c.Assert(err, IsNil)
c.Assert(notif, NotNil)
c.Check(notif.Vibration(fbck), IsNil, Commentf("not nil Vibration() for: %s", s))
c.Check(notif.Vibration(fbck), IsNil, Commentf("not nil second call to Vibration() for: %s", s))
}
}
func (*outSuite) TestGoodVibe(c *C) {
var notif *Notification
err := json.Unmarshal([]byte(`{"vibrate": {"pattern": [1,2,3], "repeat": 2}}`), ¬if)
c.Assert(err, IsNil)
c.Assert(notif, NotNil)
c.Check(notif.Vibration(nil), DeepEquals, &Vibration{Pattern: []uint32{1, 2, 3}, Repeat: 2})
}
func (*outSuite) TestGoodSimpleVibe(c *C) {
var notif *Notification
fallback := &Vibration{Pattern: []uint32{100, 100}, Repeat: 3}
err := json.Unmarshal([]byte(`{"vibrate": true}`), ¬if)
c.Assert(err, IsNil)
c.Assert(notif, NotNil)
c.Check(notif.Vibration(fallback), Equals, fallback)
}
func (*outSuite) TestBadSoundBegetsNoSound(c *C) {
c.Check((&Notification{RawSound: json.RawMessage("foo")}).Sound("x"), Equals, "")
}
func (*outSuite) TestNilSoundBegetsNoSound(c *C) {
c.Check((&Notification{RawSound: nil}).Sound("x"), Equals, "")
}
func (*outSuite) TestGoodSound(c *C) {
c.Check((&Notification{RawSound: json.RawMessage(`"foo"`)}).Sound("x"), Equals, "foo")
}
func (*outSuite) TestGoodSimpleSound(c *C) {
c.Check((&Notification{RawSound: json.RawMessage(`true`)}).Sound("x"), Equals, "x")
c.Check((&Notification{RawSound: json.RawMessage(`false`)}).Sound("x"), Equals, "")
}
ubuntu-push-0.68+16.04.20160310.2/launch_helper/helper_test.go 0000644 0000156 0000165 00000005023 12670364255 024133 0 ustar pbuser pbgroup 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 launch_helper
import (
"encoding/json"
"testing"
"time"
. "launchpad.net/gocheck"
"launchpad.net/ubuntu-push/click"
clickhelp "launchpad.net/ubuntu-push/click/testing"
helpers "launchpad.net/ubuntu-push/testing"
)
func Test(t *testing.T) { TestingT(t) }
type runnerSuite struct {
testlog *helpers.TestLogger
app *click.AppId
}
var _ = Suite(&runnerSuite{})
func (s *runnerSuite) SetUpTest(c *C) {
s.testlog = helpers.NewTestLogger(c, "error")
s.app = clickhelp.MustParseAppId("com.example.test_test-app_0")
}
func (s *runnerSuite) TestTrivialPoolWorks(c *C) {
notif := &Notification{RawSound: json.RawMessage(`"42"`), Tag: "foo"}
triv := NewTrivialHelperPool(s.testlog)
ch := triv.Start()
in := &HelperInput{App: s.app, Payload: []byte(`{"message": {"m":42}, "notification": {"sound": "42", "tag": "foo"}}`)}
triv.Run("klick", in)
out := <-ch
c.Assert(out, NotNil)
c.Check(out.Message, DeepEquals, json.RawMessage(`{"m":42}`))
c.Check(out.Notification, DeepEquals, notif)
c.Check(out.Input, DeepEquals, in)
}
func (s *runnerSuite) TestTrivialPoolWorksOnBadInput(c *C) {
triv := NewTrivialHelperPool(s.testlog)
ch := triv.Start()
msg := []byte(`{card: 3}`)
in := &HelperInput{App: s.app, Payload: msg}
triv.Run("klick", in)
out := <-ch
c.Assert(out, NotNil)
c.Check(out.Notification, IsNil)
c.Check(out.Message, DeepEquals, json.RawMessage(msg))
c.Check(out.Input, DeepEquals, in)
}
func (s *runnerSuite) TestTrivialPoolDoesNotBlockEasily(c *C) {
triv := NewTrivialHelperPool(s.testlog)
triv.Start()
msg := []byte(`this is a not your grandmother's json message`)
in := &HelperInput{App: s.app, Payload: msg}
flagCh := make(chan bool)
go func() {
// stuff several in there
triv.Run("klick", in)
triv.Run("klick", in)
triv.Run("klick", in)
flagCh <- true
}()
select {
case <-flagCh:
// whee
case <-time.After(10 * time.Millisecond):
c.Fatal("runner blocked too easily")
}
}
ubuntu-push-0.68+16.04.20160310.2/launch_helper/legacy/ 0000755 0000156 0000165 00000000000 12670364532 022530 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/launch_helper/legacy/legacy.go 0000644 0000156 0000165 00000004771 12670364255 024336 0 ustar pbuser pbgroup 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 legacy implements a HelperLauncher for “legacy†applications.
package legacy
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"strconv"
"launchpad.net/ubuntu-push/click"
"launchpad.net/ubuntu-push/logger"
)
type legacyHelperLauncher struct {
log logger.Logger
done func(string)
}
func New(log logger.Logger) *legacyHelperLauncher {
return &legacyHelperLauncher{log: log}
}
func (lhl *legacyHelperLauncher) InstallObserver(done func(string)) error {
lhl.done = done
return nil
}
var legacyHelperDir = "/usr/lib/ubuntu-push-client/legacy-helpers"
func (lhl *legacyHelperLauncher) HelperInfo(app *click.AppId) (string, string) {
return "", filepath.Join(legacyHelperDir, app.Application)
}
func (*legacyHelperLauncher) RemoveObserver() error { return nil }
type msg struct {
id string
err error
}
func (lhl *legacyHelperLauncher) Launch(appId, progname, f1, f2 string) (string, error) {
comm := make(chan msg)
go func() {
cmd := exec.Command(progname, f1, f2)
var stdout bytes.Buffer
cmd.Stdout = &stdout
var stderr bytes.Buffer
cmd.Stderr = &stderr
err := cmd.Start()
if err != nil {
comm <- msg{"", err}
return
}
proc := cmd.Process
if proc == nil {
panic("cmd.Process is nil after successful cmd.Start()??")
}
id := strconv.FormatInt((int64)(proc.Pid), 36)
comm <- msg{id, nil}
p_err := cmd.Wait()
if p_err != nil {
// Helper failed or got killed, log output/errors
lhl.log.Errorf("legacy helper failed: appId: %v, helper: %v, pid: %v, error: %v, stdout: %#v, stderr: %#v.",
appId, progname, id, p_err, stdout.String(), stderr.String())
}
lhl.done(id)
}()
msg := <-comm
return msg.id, msg.err
}
func (lhl *legacyHelperLauncher) Stop(_, id string) error {
pid, err := strconv.ParseInt(id, 36, 0)
if err != nil {
return err
}
proc, err := os.FindProcess(int(pid))
if err != nil {
return err
}
return proc.Kill()
}
ubuntu-push-0.68+16.04.20160310.2/launch_helper/legacy/legacy_test.go 0000644 0000156 0000165 00000007203 12670364255 025366 0 ustar pbuser pbgroup 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 legacy
import (
"io/ioutil"
"path/filepath"
"testing"
"time"
. "launchpad.net/gocheck"
clickhelp "launchpad.net/ubuntu-push/click/testing"
helpers "launchpad.net/ubuntu-push/testing"
)
func takeNext(ch chan string, c *C) string {
select {
case s := <-ch:
return s
case <-time.After(5 * time.Second):
c.Fatal("timed out waiting for value")
return ""
}
}
func Test(t *testing.T) { TestingT(t) }
type legacySuite struct {
lhl *legacyHelperLauncher
log *helpers.TestLogger
}
var _ = Suite(&legacySuite{})
func (ls *legacySuite) SetUpTest(c *C) {
ls.log = helpers.NewTestLogger(c, "info")
ls.lhl = New(ls.log)
}
func (ls *legacySuite) TestInstallObserver(c *C) {
c.Check(ls.lhl.done, IsNil)
c.Check(ls.lhl.InstallObserver(func(string) {}), IsNil)
c.Check(ls.lhl.done, NotNil)
}
func (s *legacySuite) TestHelperInfo(c *C) {
appname := "ubuntu-system-settings"
app := clickhelp.MustParseAppId("_" + appname)
hid, hex := s.lhl.HelperInfo(app)
c.Check(hid, Equals, "")
c.Check(hex, Equals, filepath.Join(legacyHelperDir, appname))
}
func (ls *legacySuite) TestLaunch(c *C) {
ch := make(chan string, 1)
c.Assert(ls.lhl.InstallObserver(func(id string) { ch <- id }), IsNil)
d := c.MkDir()
f1 := filepath.Join(d, "one")
f2 := filepath.Join(d, "two")
d1 := []byte(`potato`)
c.Assert(ioutil.WriteFile(f1, d1, 0644), IsNil)
exe := helpers.ScriptAbsPath("trivial-helper.sh")
id, err := ls.lhl.Launch("", exe, f1, f2)
c.Assert(err, IsNil)
c.Check(id, Not(Equals), "")
id2 := takeNext(ch, c)
c.Check(id, Equals, id2)
d2, err := ioutil.ReadFile(f2)
c.Assert(err, IsNil)
c.Check(string(d2), Equals, string(d1))
}
func (ls *legacySuite) TestLaunchFails(c *C) {
_, err := ls.lhl.Launch("", "/does/not/exist", "", "")
c.Assert(err, NotNil)
}
func (ls *legacySuite) TestHelperFails(c *C) {
ch := make(chan string, 1)
c.Assert(ls.lhl.InstallObserver(func(id string) { ch <- id }), IsNil)
_, err := ls.lhl.Launch("", "/bin/false", "", "")
c.Assert(err, IsNil)
takeNext(ch, c)
c.Check(ls.log.Captured(), Matches, "(?si).*Legacy helper failed.*")
}
func (ls *legacySuite) TestHelperFailsLog(c *C) {
ch := make(chan string, 1)
c.Assert(ls.lhl.InstallObserver(func(id string) { ch <- id }), IsNil)
exe := helpers.ScriptAbsPath("noisy-helper.sh")
_, err := ls.lhl.Launch("", exe, "", "")
c.Assert(err, IsNil)
takeNext(ch, c)
c.Check(ls.log.Captured(), Matches, "(?s).*BOOM-1.*")
c.Check(ls.log.Captured(), Matches, "(?s).*BANG-1.*")
c.Check(ls.log.Captured(), Matches, "(?s).*BOOM-20.*")
c.Check(ls.log.Captured(), Matches, "(?s).*BANG-20.*")
}
func (ls *legacySuite) TestStop(c *C) {
ch := make(chan string, 1)
c.Assert(ls.lhl.InstallObserver(func(id string) { ch <- id }), IsNil)
// exe := helpers.ScriptAbsPath("slow-helper.sh")
id, err := ls.lhl.Launch("", "/bin/sleep", "9", "1")
c.Assert(err, IsNil)
err = ls.lhl.Stop("", "===")
c.Check(err, NotNil) // not a valid id
err = ls.lhl.Stop("", id)
c.Check(err, IsNil)
takeNext(ch, c)
err = ls.lhl.Stop("", id)
c.Check(err, NotNil) // no such processs
}
ubuntu-push-0.68+16.04.20160310.2/protocol/ 0000755 0000156 0000165 00000000000 12670364532 020314 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/protocol/state-diag-session.svg 0000644 0000156 0000165 00000033534 12670364255 024552 0 ustar pbuser pbgroup 0000000 0000000
ubuntu-push-0.68+16.04.20160310.2/protocol/messages_test.go 0000644 0000156 0000165 00000012546 12670364255 023523 0 ustar pbuser pbgroup 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.Type = ""
b.Reset()
c.Check(b.Type, Equals, "broadcast")
c.Check(b.splitting, Equals, 0)
}
func (s *messagesSuite) TestConnBrokenMsg(c *C) {
m := &ConnBrokenMsg{}
c.Check(m.Split(), Equals, true)
c.Check(m.OnewayContinue(), Equals, false)
}
func (s *messagesSuite) TestConnWarnMsg(c *C) {
m := &ConnWarnMsg{}
c.Check(m.Split(), Equals, true)
c.Check(m.OnewayContinue(), Equals, true)
}
func (s *messagesSuite) TestSetParamsMsg(c *C) {
m := &SetParamsMsg{}
c.Check(m.Split(), Equals, true)
c.Check(m.OnewayContinue(), Equals, true)
}
func (s *messagesSuite) TestExtractPayloads(c *C) {
c.Check(ExtractPayloads(nil), IsNil)
p1 := json.RawMessage(`{"a":1}`)
p2 := json.RawMessage(`{"b":2}`)
ns := []Notification{Notification{Payload: p1}, Notification{Payload: p2}}
c.Check(ExtractPayloads(ns), DeepEquals, []json.RawMessage{p1, p2})
}
func (s *messagesSuite) TestSplitNotificationsMsgNop(c *C) {
n := &NotificationsMsg{
Type: "notifications",
Notifications: []Notification{
Notification{"app1", "msg1", json.RawMessage(`{m:1}`)},
Notification{"app1", "msg1", json.RawMessage(`{m:2}`)},
},
}
done := n.Split()
c.Check(done, Equals, true)
c.Check(cap(n.Notifications), Equals, 2)
c.Check(len(n.Notifications), Equals, 2)
}
var payloadFmt2 = fmt.Sprintf(`{"b":%%d,"bloat":"%s"}`, strings.Repeat("x", 1024*2-notificationOverhead-4-6)) // 4 = app1 6 = msg%03d
func manyNotifications(c int) []Notification {
notifs := make([]Notification, 0, 1)
for i := 0; i < c; i++ {
notifs = append(notifs, Notification{
"app1",
fmt.Sprintf("msg%03d", i),
json.RawMessage(fmt.Sprintf(payloadFmt2, i)),
})
}
return notifs
}
func (s *messagesSuite) TestSplitNotificationsMsgMany(c *C) {
notifs := manyNotifications(33)
n := len(notifs)
// more interesting this way
c.Assert(cap(notifs), Not(Equals), n)
nm := &NotificationsMsg{
Type: "notifications",
Notifications: notifs,
}
done := nm.Split()
c.Assert(done, Equals, false)
n1 := len(nm.Notifications)
buf, err := json.Marshal(nm)
c.Assert(err, IsNil)
c.Assert(len(buf) <= 65535, Equals, true)
c.Check(len(buf)+len(notifs[n1].Payload) > maxPayloadSize, Equals, true)
done = nm.Split()
c.Assert(done, Equals, true)
n2 := len(nm.Notifications)
c.Check(n1+n2, Equals, n)
notifs = manyNotifications(61)
n = len(notifs)
nm = &NotificationsMsg{
Type: "notifications",
Notifications: notifs,
}
done = nm.Split()
c.Assert(done, Equals, false)
n1 = len(nm.Notifications)
done = nm.Split()
c.Assert(done, Equals, false)
n2 = len(nm.Notifications)
done = nm.Split()
c.Assert(done, Equals, true)
n3 := len(nm.Notifications)
c.Check(n1+n2+n3, Equals, n)
// reset
nm.Type = ""
nm.Reset()
c.Check(nm.Type, Equals, "notifications")
c.Check(nm.splitting, Equals, 0)
}
ubuntu-push-0.68+16.04.20160310.2/protocol/state-diag-client.gv 0000644 0000156 0000165 00000001430 12670364255 024150 0 ustar pbuser pbgroup 0000000 0000000 digraph state_diagram_client {
label = "State diagram for client";
size="12,6";
rankdir=LR;
node [shape = doublecircle]; pingTimeout; connBroken;
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"];
loop -> connBroken [label = "Read CONNBROKEN"];
loop -> warn [label = "Read CONNWARN"];
warn -> loop;
}
ubuntu-push-0.68+16.04.20160310.2/protocol/protocol_test.go 0000644 0000156 0000165 00000014107 12670364255 023550 0 ustar pbuser pbgroup 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.68+16.04.20160310.2/protocol/state-diag-client.svg 0000644 0000156 0000165 00000020315 12670364255 024336 0 ustar pbuser pbgroup 0000000 0000000
ubuntu-push-0.68+16.04.20160310.2/protocol/messages.go 0000644 0000156 0000165 00000012356 12670364255 022463 0 ustar pbuser pbgroup 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"
"fmt"
)
// 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
Cookie 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)
}
// OnewayMsg are messages that are not to be followed by a response,
// after sending them the session either aborts or continues.
type OnewayMsg interface {
SplittableMsg
// continue session after the message?
OnewayContinue() 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
}
func (m *ConnBrokenMsg) OnewayContinue() bool {
return false
}
// CONNBROKEN reasons
const (
BrokenHostMismatch = "host-mismatch"
)
// CONNWARN message, server side is warning about partial functionality
// because reason.
type ConnWarnMsg struct {
Type string `json:"T"`
// reason
Reason string
}
func (m *ConnWarnMsg) Split() bool {
return true
}
func (m *ConnWarnMsg) OnewayContinue() bool {
return true
}
// CONNWARN reasons
const (
WarnUnauthorized = "unauthorized"
)
// SETPARAMS message
type SetParamsMsg struct {
Type string `json:"T"`
SetCookie string
}
func (m *SetParamsMsg) Split() bool {
return true
}
func (m *SetParamsMsg) OnewayContinue() bool {
return true
}
// 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 and sets the proper Type.
func (b *BroadcastMsg) Reset() {
b.Type = "broadcast"
b.splitting = 0
}
// NOTIFICATIONS message
type NotificationsMsg struct {
Type string `json:"T"`
Notifications []Notification
splitting int
}
// Reset resets the splitting state if the message storage is to be
// reused and sets the proper Type.
func (m *NotificationsMsg) Reset() {
m.Type = "notifications"
m.splitting = 0
}
func (m *NotificationsMsg) Split() bool {
if m.splitting != 0 {
m.Notifications = m.Notifications[len(m.Notifications):m.splitting]
}
notifs := m.Notifications
var size int
for i, notif := range notifs {
size += len(notif.Payload) + len(notif.AppId) + len(notif.MsgId) + notificationOverhead
if size > maxPayloadSize {
m.splitting = len(notifs)
m.Notifications = notifs[:i]
return false
}
}
return true
}
var notificationOverhead int
func init() {
buf, err := json.Marshal(Notification{})
if err != nil {
panic(fmt.Errorf("failed to compute Notification marshal overhead: %v", err))
}
notificationOverhead = len(buf) - 4 // - 4 for the null from P(ayload)
}
// A single unicast notification
type Notification struct {
AppId string `json:"A"`
MsgId string `json:"M"`
// payload
Payload json.RawMessage `json:"P"`
}
// ExtractPayloads gets only the payloads out of a slice of notications.
func ExtractPayloads(notifications []Notification) []json.RawMessage {
n := len(notifications)
if n == 0 {
return nil
}
payloads := make([]json.RawMessage, n)
for i := 0; i < n; i++ {
payloads[i] = notifications[i].Payload
}
return payloads
}
// ACKnowledgement message
type AckMsg struct {
Type string `json:"T"`
}
// xxx ... query levels messages
ubuntu-push-0.68+16.04.20160310.2/protocol/protocol.go 0000644 0000156 0000165 00000005722 12670364255 022514 0 ustar pbuser pbgroup 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.68+16.04.20160310.2/protocol/state-diag-session.gv 0000644 0000156 0000165 00000002734 12670364255 024365 0 ustar pbuser pbgroup 0000000 0000000 digraph state_diagram_session {
label = "State diagram for session";
size="12,6";
rankdir=LR;
node [shape = doublecircle]; stop;
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"];
// other
loop -> conn_broken [label = "Receive connbroken request"];
loop -> conn_warn [label = "Receive connwarn request"];
conn_broken -> stop [label = "Write CONNBROKEN"];
conn_warn -> loop [label = "Write CONNWARN"];
// timeouts
ack_wait -> stop [label = "Elapsed exhange timeout"];
split_ack_wait -> stop [label = "Elapsed exhange timeout"];
pong_wait -> stop [label = "Elapsed exhange timeout"];
}
ubuntu-push-0.68+16.04.20160310.2/docs/ 0000755 0000156 0000165 00000000000 12670364532 017403 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/docs/highlevel.txt 0000644 0000156 0000165 00000006754 12670364255 022131 0 ustar pbuser pbgroup 0000000 0000000 Ubuntu Push Client High Level Developer Guide
=============================================
:Version: 0.50+
.. contents::
Introduction
------------
This document describes how to use the Ubuntu Push Client service from the point of view of a developer writing
a QML-based application.
---------
.. include:: _description.txt
The PushClient Component
------------------------
Example::
import Ubuntu.PushNotifications 0.1
PushClient {
id: pushClient
Component.onCompleted: {
notificationsChanged.connect(messageList.handle_notifications)
error.connect(messageList.handle_error)
}
appId: "com.ubuntu.developer.push.hello_hello"
}
Registration: the appId and token properties
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To register with the push system and start receiving notifications, set the ``appId`` property to your application's APP_ID,
with or without version number. For this to succeed the user **must** have an Ubuntu One account configured in the device.
The APP_ID is as described in the `ApplicationId documentation `__
except that the version is treated as optional. Therefore both ``com.ubuntu.music_music`` and ``com.ubuntu.music_music_1.3.496``
are valid. Keep in mind that while both versioned and unversioned APP_IDs are valid, they are still different and will affect
which notifications are delivered to the application. Unversioned IDs mean the token will be the same after updates and the application
will receive old notifications, while versioned IDs mean the app needs to explicitly ask to get older messages delivered.
Setting the same appId more than once has no effect.
After you are registered, if no error occurs, the PushClient will have a value set in its ``token`` property
which uniquely identifies the user+device combination.
Receiving Notifications
~~~~~~~~~~~~~~~~~~~~~~~
When a notification is received by the Push Client, it will be delivered to your application's push helper, and then
placed in your application's mailbox. At that point, the PushClient will emit the ``notificationsChanged(QStringList)`` signal
containing your messages. You should probably connect to that signal and handle those messages.
Because of the application's lifecycle, there is no guarantee that it will be running when the signal is emitted. For that
reason, apps should check for pending notifications whenever they are activated or started. To do that, use the
``getNotifications()`` slot. Triggering that slot will fetch notifications and trigger the
``notificationsChanged(QStringList)`` signal.
Error Handling
~~~~~~~~~~~~~~
Whenever PushClient suffers an error, it will emit the ``error(QString)`` signal with the error message.
Persistent Notification Management
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Some notifications are persistent, meaning that, after they are presented, they don't disappear automatically.
This API allows the app to manage that type of notifications.
On each notification there's an optional ``tag`` field, used for this purpose.
The ``persistent`` property of PushClient contains the list of the tags of notifications with the "persist" element set
to true that are visible to the user right now.
The ``void clearPersistent(QStringList tags)`` method clears persistent notifications for that app marked by ``tags``.
If no tag is given, match all.
The ``count`` property sets the counter in the application's icon to the given value.
.. include:: _common.txt
ubuntu-push-0.68+16.04.20160310.2/docs/_common.txt 0000644 0000156 0000165 00000022001 12670364255 021570 0 ustar pbuser pbgroup 0000000 0000000 Application Helpers
-------------------
The payload delivered to push-client will be passed onto a helper program that can modify it as needed before passing it onto
the postal service (see `Helper Output Format <#helper-output-format>`__).
The helper receives two arguments ``infile`` and ``outfile``. The message is delivered via ``infile`` and the transformed
version is placed in ``outfile``.
This is the simplest possible useful helper, which simply passes the message through unchanged:
.. include:: example-client/helloHelper
:literal:
Helpers need to be added to the click package manifest:
.. include:: example-client/manifest.json
:literal:
Here, we created a helloHelper entry in hooks that has an apparmor profile and an additional JSON file for the push-helper hook.
helloHelper-apparmor.json must contain **only** the push-notification-client policy group and the ubuntu-push-helper template:
.. include:: example-client/helloHelper-apparmor.json
:literal:
And helloHelper.json must have at least a exec key with the path to the helper executable relative to the json, and optionally
an app_id key containing the short id of one of the apps in the package (in the format packagename_appname without a version).
If the app_id is not specified, the helper will be used for all apps in the package::
{
"exec": "helloHelper",
"app_id": "com.ubuntu.developer.ralsina.hello_hello"
}
.. note:: For deb packages, helpers should be installed into /usr/lib/ubuntu-push-client/legacy-helpers/ as part of the package.
Helper Output Format
--------------------
Helpers output has two parts, the postal message (in the "message" key) and a notification to be presented to the user (in the "notification" key).
.. note:: This format **will** change with future versions of the SDK and it **may** be incompatible.
Here's a simple example::
{
"message": "foobar",
"notification": {
"tag": "foo",
"card": {
"summary": "yes",
"body": "hello",
"popup": true,
"persist": true,
"timestamp": 1407160197
}
"sound": "buzz.mp3",
"vibrate": {
"pattern": [200, 100],
"repeat": 2
}
"emblem-counter": {
"count": 12,
"visible": true
}
}
}
The notification can contain a **tag** field, which can later be used by the `persistent notification management API. <#persistent-notification-management>`__
:message: (optional) A JSON object that is passed as-is to the application via PopAll.
:notification: (optional) Describes the user-facing notifications triggered by this push message.
The notification can contain a **card**. A card describes a specific notification to be given to the user,
and has the following fields:
:summary: (required) a title. The card will not be presented if this is missing.
:body: longer text, defaults to empty.
:actions: If empty (the default), a bubble notification is non-clickable.
If you add a URL, then bubble notifications are clickable and launch that URL. One use for this is using a URL like
``appid://com.ubuntu.developer.ralsina.hello/hello/current-user-version`` which will switch to the app or launch
it if it's not running. See `URLDispatcher `__ for more information.
:icon: An icon relating to the event being notified. Defaults to empty (no icon);
a secondary icon relating to the application will be shown as well, regardless of this field.
:timestamp: Seconds since the unix epoch, only used for persist (for now). If zero or unset, defaults to current timestamp.
:persist: Whether to show in notification centre; defaults to false
:popup: Whether to show in a bubble. Users can disable this, and can easily miss them, so don't rely on it exclusively. Defaults to false.
.. note:: Keep in mind that the precise way in which each field is presented to the user depends on factors such as
whether it's shown as a bubble or in the notification centre, or even the version of Ubuntu Touch the user
has on their device.
The notification can contain a **sound** field. This is either a boolean (play a predetermined sound) or the path to a sound file. The user can disable it, so don't rely on it exclusively.
Defaults to empty (no sound). The path is relative, and will be looked up in (a) the application's .local/share/, and (b)
standard xdg dirs.
The notification can contain a **vibrate** field, causing haptic feedback, which can be either a boolean (if true, vibrate a predetermined way) or an object that has the following content:
:pattern: a list of integers describing a vibration pattern (duration of alternating vibration/no vibration times, in milliseconds).
:repeat: number of times the pattern has to be repeated (defaults to 1, 0 is the same as 1).
The notification can contain a **emblem-counter** field, with the following content:
:count: a number to be displayed over the application's icon in the launcher.
:visible: set to true to show the counter, or false to hide it.
.. note:: Unlike other notifications, emblem-counter needs to be cleaned by the app itself.
Please see `the persistent notification management section. <#persistent-notification-management>`__
.. FIXME crosslink to hello example app on each method
Security
~~~~~~~~
To use the push API, applications need to request permission in their security profile, using something like this:
.. include:: example-client/hello.json
:literal:
Ubuntu Push Server API
----------------------
The Ubuntu Push server is located at https://push.ubuntu.com and has a single endpoint: ``/notify``.
To notify a user, your application has to do a POST with ``Content-type: application/json``.
.. note:: The contents of the data field are arbitrary. They should be enough for your helper to build
a notification using it, and decide whether it should be displayed or not. Keep in mind
that this will be processed by more than one version of the helper, because the user may be using
an older version of your app.
Here is an example of the POST body using all the fields::
{
"appid": "com.ubuntu.music_music",
"expire_on": "2014-10-08T14:48:00.000Z",
"token": "LeA4tRQG9hhEkuhngdouoA==",
"clear_pending": true,
"replace_tag": "tagname",
"data": {
"id": 43578,
"timestamp": 1409583746,
"serial": 1254,
"sender": "Joe",
"snippet": "Hi there!"
}
}
:appid: ID of the application that will receive the notification, as described in the client side documentation.
:expire_on: Expiration date/time for this message, in `ISO8601 Extendend format `__
:token: The token identifying the user+device to which the message is directed, as described in the client side documentation.
:clear_pending: Discards all previous pending notifications. Usually in response to getting a "too-many-pending" error.
:replace_tag: If there's a pending notification with the same tag, delete it before queuing this new one.
:data: A JSON object.
Limitations of the Server API
-----------------------------
The push notification infrastructure is meant to help ensuring timely
delivery of application notifications if the device is online or
timely informing the device user about application notifications that
were pending when the device comes back online. This in the face of
applications not being allowed to be running all the time, and
avoiding the resource cost of many applications all polling different services
frequently.
The push notification infrastructure is architected to guarantee at
least best-effort with respect to these goals and beyond it, on the
other end applications should not expect to be able to use and only
rely on the push notification infrastructure to store application
messages if they want ensure all their notification or messages are
delivered, the infrastructure is not intended to be the only long term
"inbox" storage for an application.
To preserve overall throughput the infrastructure imposes some limits
on applications:
* message data payload is limited to 2K
* when inserted all messages need to specify an expiration date after
which they can be dropped and not delivered
* an application is limited in the number of messages per token
(application/user/device combination) that can be undelivered/pending at the
same time (100 currently)
replace_tag can be used to implement notifications for which the newest
one replace the previous one if pending.
clear_pending can be used to be deal with a pending message limit
reached, possibly substituting the current undelivered messages with a
more generic one.
Applications using the push notification HTTP API should be robust
against receiving 503 errors, retrying after waiting with increasing
back-off. Later rate limits (signaled with the 429 status) may also come
into play.
ubuntu-push-0.68+16.04.20160310.2/docs/lowlevel.txt 0000644 0000156 0000165 00000017566 12670364255 022016 0 ustar pbuser pbgroup 0000000 0000000 Ubuntu Push Client Low Level Developer Guide
============================================
:Version: 0.50+
.. contents::
Introduction
------------
This document describes how to use the Ubuntu Push Client service from a platform integrator's point of view.
Application developers are expected to use a much simpler API, in turn based on the lower-level API described here.
The expected audience for this document is, therefore, either platform developers, or application developers who,
for whatever reason, can't use or prefer not to use the available higher level APIs.
---------
.. include:: _description.txt
The PushNotifications Service
-----------------------------
:Service: com.ubuntu.PushNotifications
:Object path: /com/ubuntu/PushNotifications/QUOTED_PKGNAME
The PushNotifications service handles registering the device with the Ubuntu Push service to enable delivery of messages to
it.
Each Ubuntu Touch package has to use a separate object path for security reasons, that's why the object path includes QUOTED_PKGNAME.
For example, in the case of the music application, the package name is ``com.ubuntu.music`` and QUOTED_PKGNAME is com_2eubuntu_2emusic.
Everything that is not a letter or digit has to be quoted as _XX where XX are the hex digits of the character. In practice,
this means replacing "." with "_2e" and "-" with "_2d"
.. note:: For applications that are not installed as part of click packages, the QUOTED_PKGNAME is "_" and the APP_ID when required is
_PACKAGENAME.
For example, for ubuntu-system-settins:
* QUOTED_PKGNAME is _
* APP_ID is _ubuntu-system-settings
com.ubuntu.PushNotifications.Register
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``string Register(string APP_ID)``
Example::
$ gdbus call --session --dest com.ubuntu.PushNotifications --object-path /com/ubuntu/PushNotifications/com_2eubuntu_2emusic \
--method com.ubuntu.PushNotifications.Register com.ubuntu.music_music
('LeA4tRQG9hhEkuhngdouoA==',)
The Register method takes as argument the APP_ID (in the example, com.ubuntu.music_music) and returns a token identifying the user
and device. For this to succeed the user **must** have an Ubuntu One account configured in the device.
In the case the Register method returns a "bad auth" error, the application should inform the user to generate new Ubuntu One tokens.
The APP_ID is as described in the `ApplicationId documentation `__
except that the version is treated as optional. Therefore both ``com.ubuntu.music_music`` and ``com.ubuntu.music_music_1.3.496``
are valid. Keep in mind that while both versioned and unversioned APP_IDs are valid, they are still different and will affect
which notifications are delivered to the application. Unversioned IDs mean the token will be the same after updates and the application
will receive old notifications, while versioned IDs mean the app needs to explicitly ask to get older messages delivered.
Register is idempotent, and calling it multiple times returns the same token.
This token is later used by the application server to indicate the recipient of notifications.
.. FIXME crosslink to server app
.. note:: There is currently no way to send a push message to all of a user's devices. The application server has to send to
each registered device individually instead.
com.ubuntu.PushNotifications.Unregister
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``void Unregister(string APP_ID)``
Example::
$ gdbus call --session --dest com.ubuntu.PushNotifications --object-path /com/ubuntu/PushNotifications/com_2eubuntu_2emusic \
--method com.ubuntu.PushNotifications.Unregister com.ubuntu.music_music
The Unregister method invalidates the token obtained via `Register <#com-ubuntu-pushnotifications-register>`_ therefore disabling
reception of push messages.
The method takes as argument the APP_ID (in the example, com.ubuntu.music_music) and returns nothing.
The APP_ID is as described in the `ApplicationId documentation `__
except that the version is treated as optional. Therefore both ``com.ubuntu.music_music`` and ``com.ubuntu.music_music_1.3.496``
are valid.
The Postal Service
------------------
:Service: com.ubuntu.Postal
:Object path: /com/ubuntu/Postal/QUOTED_PKGNAME
The Postal service delivers the actual messages to the applications. After the application is registered, the push client will begin
delivering messages to the device, which will then (possibly) cause specific notifications to be presented to the user (message bubbles,
sounds, haptic feedbak, etc.) Regardless of whether the user acknowledges those notifications or not, the payload of the push message
is put in the Postal service for the application to pick up.
Because user response to notifications can cause application activation, apps should check the status of the Postal service every time
the application activates.
com.ubuntu.Postal.Post
~~~~~~~~~~~~~~~~~~~~~~
``void Post(string APP_ID, string message)``
Example::
gdbus call --session --dest com.ubuntu.Postal --object-path /com/ubuntu/Postal/com_2eubuntu_2emusic \
--method com.ubuntu.Postal.Post com.ubuntu.music_music \
'"{\"message\": \"foobar\", \"notification\":{\"card\": {\"summary\": \"yes\", \"body\": \"hello\", \"popup\": true, \"persist\": true}}}"'
The arguments for the Post method are APP_ID (in the example, com.ubuntu.music_music) and a JSON string
`describing a push message. <#helper-output-format>`__
Depending on the contents of the push message it may trigger user-facing notifications, and will queue a
message for the app to get via the `PopAll <#com-ubuntu-postal-popalls>`__ method.
The APP_ID is as described in the `ApplicationId documentation `__
except that the version is treated as optional. Therefore both ``com.ubuntu.music_music`` and ``com.ubuntu.music_music_1.3.496``
are valid.
.. note:: Post is useful as a unified frontend for notifications in Ubuntu Touch, since it wraps and abstracts several different APIs.
com.ubuntu.Postal.PopAll
~~~~~~~~~~~~~~~~~~~~~~~~
``array{string} PopAll(string APP_ID)``
Example::
$ gdbus call --session --dest com.ubuntu.Postal --object-path /com/ubuntu/Postal/com_2eubuntu_2emusic \
--method com.ubuntu.Postal.PopAll com.ubuntu.music_music
(['{"foo": "bar", ....}'],)
The argument for the PopAll method is the APP_ID and it returns a list of strings, each string being a separate postal
message, the "message" element of a helper's output fed from `Post <#com-ubuntu-postal-post>`__
or from the Ubuntu Push service,
Post Signal
~~~~~~~~~~~
``void Post(string APP_ID)``
Every time a notification is posted, the postal service will emit the Post signal. Your app can connect to it to react to
incoming notifications if it's running when they arrive. Remember that on Ubuntu Touch, the application lifecycle means
it will often **not** be running when notifications arrive. If the application is in the foreground when a notification
arrives, the notification **will not** be presented.
The object path is similar to that of the Postal service methods, containing the QUOTED_PKGNAME.
Persistent Notification Management
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Some notifications are persistent, meaning that, after they are presented, they don't disappear automatically.
This API allows the app to manage that type of notifications.
On each notification there's an optional ``tag`` field, used for this purpose.
``array(string) ListPersistent(string APP_ID)``
Returns a list of the tags of notifications with the "persist" element set to true that are visible to the user right now.
``void ClearPersistent(string APP_ID, [tag1, tag2,....])``
Clears persistent notifications for that app by tag(s). If none given, match all.
``void SetCounter(string APP_ID, int count int, bool visible)``
Set the counter to the given values.
.. include:: _common.txt
ubuntu-push-0.68+16.04.20160310.2/docs/_description.txt 0000644 0000156 0000165 00000005052 12670364255 022632 0 ustar pbuser pbgroup 0000000 0000000 Let's describe the push system by way of an example.
Alice has written a chat application called Chatter. Using it, Bob can send messages to Carol and viceversa. Alice has a
web application for it, so the way it works now is that Bob connects to the service, posts a message, and when Carol
connects, she gets it. If Carol leaves the browser window open, it beeps when messages arrive.
Now Alice wants to create an Ubuntu Touch app for Chatter, so she implements the same architecture using a client that
does the same thing as the web browser. Sadly, since applications on Ubuntu Touch don't run continuously, messages are
only delivered when Carol opens the app, and the user experience suffers.
Using the Ubuntu Push Server, this problem is alleviated: the Chatter server will deliver the messages to the Ubuntu
Push Server, which in turn will send it in an efficient manner to the Ubuntu Push Client running in Bob and Carol's
devices. The user sees a notification (all without starting the app) and then can launch it if he's interested in
reading messages at that point.
Since the app is not started and messages are delivered oportunistically, this is both battery and bandwidth-efficient.
.. figure:: push.svg
The Ubuntu Push system provides:
* A push server which receives **push messages** from the app servers, queues them and delivers them efficiently
to the devices.
* A push client which receives those messages, queues messages to the app and displays notifications to the user
The full lifecycle of a push message is:
* Created in a application-specific server
* Sent to the Ubuntu Push server, targeted at a user or user+device pair
* Delivered to one or more Ubuntu devices
* Passed through the application helper for processing
* Notification displayed to the user (via different mechanisms)
* Application Message queued for the app's use
If the user interacts with the notification, the application is launched and should check its queue for messages
it has to process.
For the app developer, there are several components needed:
* A server that sends the **push messages** to the Ubuntu Push server
* Support in the client app for registering with the Ubuntu Push client
* Support in the client app to react to **notifications** displayed to the user and process **application messages**
* A helper program with application-specific knowledge that transforms **push messages** as needed.
In the following sections, we'll see how to implement all the client side parts. For the application server, see the
`Ubuntu Push Server API section <#ubuntu-push-server-api>`__
ubuntu-push-0.68+16.04.20160310.2/docs/example-client/ 0000755 0000156 0000165 00000000000 12670364532 022312 5 ustar pbuser pbgroup 0000000 0000000 ubuntu-push-0.68+16.04.20160310.2/docs/example-client/push-example.png 0000644 0000156 0000165 00000057504 12670364255 025445 0 ustar pbuser pbgroup 0000000 0000000 ‰PNG
IHDR k¬XT pHYs
×
×B(›x tIMEÞ
2œ2„š PLTE ÿÿÿ ÿÿÿ ÿÿÿ ÿÿÿ qqq®®®²²²¿¿¿ÀÀÀòòòôôôÿÿÿ ,,,LLLYYYïïïæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóæææèèèêêêóóóÃÃÃÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõFI½ *tRNS ÏÐÐÐÐÐÐÐÐÐÐÐÐÐÐÑÑÜÜÚ¿Îâ bKGDÿ-Þ ]€IDATÌÁá]çu]Ñ9×>—r´¯ÑX$ý@}üÂå¢iË’HÞóíÕKÇNh(¿
õ3ù«k~áqýÒùÕË}óÅõµÌWÌ×ðkåïÄ¿’_¨üÂý žÃ_Ý||ÅòËW,&üUå*¿ð üxñ›¯X¾bùŠå+åï„Wùù…û\Sþâækåkåkå+–¿þƒüBåîðéâ/7_±|ÅòËWÊ߃ ÿ¡òòK`®ò›¯X¾R¾V¾bù;0á+ò•_¸À3üÕÍ×ÊW,_±|ÅòÿßôkûŸ8ÿ‰/Ÿ?þ»ûoœ¿±£ƒ¿óßßÿ_û§·oß}ûöíÛwß¾{ûoÞ½ýâÝßxÿîkïÿÆoÞÿ§~óþÿ¥ß¼ÿÊ=o[¶èìf÷xÊÙÒ—¥wÚžz\×÷¸{³eû,”BŸ-Ý–ž¥ºžúôpûÌvË.-çH·œ=ÐÛµÛlËòüD]ºn¹íí–.Ëöز-‡ží¶ía»m]—¶Ë¡=Çãn˺víæîñÙr8=ùóÎV7X§dËJè4Ô3lØ!›nÃÆû2àçÇ}Yi&»SDRl±cV‘½N°g>Oµƒ?ÏAªÖ=Éí!PBÞ£%èQ‘í«°ÎF‘Jh«@Û(H(ˆ`xÃ^9ÿsÞÕö~ðò,cEĦ0ƒ€¥5%ä_¿a6íN:'Û«²ŠË$%§QÔ†€:¾1i M•$Í^A¤ãòÒv¨@ÜarK%R•?ý
ð¸AÀ²dhVNðûy‡p±Û¾9fmwðžx¬–"®®P¢5u/—€GÈÆu\If%…F’<°YJâ¹+àžµr‚–ž œìÕ)6
»XlbáÆ2q9Q‚ž”÷dw”µbLÙ†„E?Ì{¬`2¼dÓ±¦¶gl"œT±2ØÁ°ƒµÚt5…Å—E”Z·¦Û±ÊûùÚ™AÝ8†&e;
E˜µ†ñÚ©/ÐÀ¸Úª€q¥ÞCmÔ!o†Äç%-Õx¤N§æmPï`ÚeÖjzœ;ô¤+ÌVBDh
ÝŒwMѧ½6--çbmÇ'©\`±È`èI!6®d×È‘Â
9
mÄ65XÁÞ‚#Ý¡¸ôD*ÜSÑw½º4˜ÒÚh%ðaÞ¯Ë|šÜÁêg»^+Uëâþ4(VÁºÊràª+ØNÇ\«Fö"Û¡nèy,„mìI]ÜÌÅhЀÍÖ@XSë¶iÜá@P^ ÛH«ØF¨`ªøÜ„†Õ‚|˜÷ØP!Bà§^¸I½¯H‡`Ó6tÀÇ•óù¢lГ:Ú5„Vèhk¥môdÚôj¥kCrïì Rz_ÛÑ©RK){.ŒE‰f‹K4TOÏŽT,$‹VŠÔTÊ¥áøÝ¼‡Ài:µwÈE*ÐwÝØ
aÁÓ´D¥•“J[0u!Ø‘°gðDØz7 ”©·n6Û@–ÈZbaj!¥‹%º“b6B÷ãšË‘ ÅÕ%ºÒ€ºé:bÑ”¦áü‡M§HHj˜êÁœ@vleÍyìtˆÕF#„9MC€æ«Q:ŸCÓÐs•)c]“I°›@"-Ü e[ZcéHÔz† nˆP´Þ¶#sêR›ÒBÓ˜=8æ‰;èç«íœp¾›÷ÚÐX+n½º=
ÙPƒ€~¾Ž´P ¨‹²-°d!¤YW®V‰›FºtP ¬Ås¹—'–±KÂ!Y»¡qÚ‚BP7œÍÐv/ââʺ„Ù‚êPtU^wÎ&‹;|˜oÏ–ûºÓâó¤{ËZôpzÝ£[wÓ-¥õ$wënhù4ÉZØc9ö°ÙvÇ¥¶6ÝxöÍ·‹[ªÀ
Ú>kieÙâK7ÏRÊòÅ6î²U¶öÈéÒ%ŠíÜÕì¹~7ߎ‹t6äháRˤ6ŒS”M6PAc´©Är²D+¶¡9v8„äf³llåEvfOšÆUA„´ìs(·Ä1µ±E9u
YŸÓ^69;jQ\ÃDhJ—úB`ã~?oÓ+íµ!žÔJî9Sè Å6Z)pïæw,:½
˜Bª€ R¯•¦¦¤ÍqšîH 0@ÏÕré鵩‡4°¢U,é™^{bèµO›¢+4´g!B òE(¿wã}IWòL7Òô$ÝÔ²¢ÍbÖÞSåŠZr=°)¡RÕµ“A·ÁÚ^½ØÁ¥Óâð¬Ed랤)½ÇCƒ™²RƒA£šÔüÙX6+¬
íøý¼ÕêÍ繺±˜Ð)°ÈaãË&c‰Os†Ààq h\8® €ZÇŠ%;MǦ»“ÒŒt/š–2¡[»C‰ö‘šDb\ÁÖ£¦t
}P‰˜NUè1ðaþ[*›}³ö¾¬q£I§!÷ìJ–„Í\UC¥b+»Ù)q7‚ˆ°‘Ò•l´
…C¨ž«ÌÇŠ¨¥b0V"iè¶R’#h)§)+ˆÑf;Tåjt…Þ¤iAÍòÛùMS|‰;ê 5m¶6md‚)Y«+4ͺ){IÓÞÊîå6(„ðg6ògG‹I}íé™òââ6°®ÿò«Ø´Bo„„‹'Kh%^¤‹DâmcaE:Ê‹Ýmà÷óÖ¢‡Í‰un{_÷ÔæŒÏ\õØRÛê*=Áž©ª×ºuãç+ž¬ÖZhi*ͱQ*µ•¢"{¥D] 5¿YBÈž¡UˆÐ«
kj[ÀÕN9d³+gLS@^¢´Ä—ßÍ;Å Êf6’UçœG4‰4VR -JŸášèNP΃
Ɇ”
E‰|AÝ™ò¢G7Ò€HÎ&Ÿ
ÔJ‰P-+k°æ„ˆb¥;u¾T©%Tí°XÁnºù0ïé¦>?½qÛ4ièfÊ¿±h´îv7Ö}Ó({f1¸4¢!YkKn#
mJ›b–9²ZF)M7d¬ÓÅJ²õL–ÐdÛm³¦nXñ Ü n@δ¤VÓd‡ý~ÞmÄÝ7‚6v¯Êîý†Lˆ©1”ÈÄvÁ›-Ó5w(³Zj¡
#‚¢ƒn:¬µ¦§Ÿ6X…Å`iv\ÛaÚˆzÆÈˆéRÖaºšU‘†½à„*~?om¸¯*lå§7Yx@Ï iƒ@HÞnZDZ›^tйÓl
µ'ÜÞ'œ(9Q
K¸“ÈŽrÊç~SÚ†â›EаAT6…Íb\¢´‘jϳ±um±„l£µi¢mŠßÍ[*a‘hM›«¹çXDÙÄ Jqјf*Þ˜6€Ó´‚wd¸ÅµâÚQv‹”Ž»™IÐMȆ#5m•ÆC›v€
Ú0novSÜô¦„†ºòBy„SÙïç-é669C¶3mSÓˆ ®Ò9kš~|¬2‹•(®‘ {-˜ö
ÎXRϰy9—íôv;R«°x^ª¡„©õIÆZuMºzJ'0u‹n,§+PhðóP9Xɇy¿žÉŠ€l”†ešÅ¦¨«²¶Z‡šzFñNq¯¶îUu³²ô²ÓÆUK¦‰`§èpcm7w²bgm¢¸—ȺWÛ7½l*6ĤŠ9‰|~‘ÅþtÝ´i?ï…Z¤BtÓœq»A6ÚŽÚbÃìç©ì4Yµ;!°Q7U1¶=vu€´Àk
Vz-mªâƒ4Ü`wéÁX»ÓçÂà’È"Ùñå`S©ˆÓhw\Ççk¾›·””(ZêzO‹ucP+Pv²è‚pôªCª¤;·¥žKE%+†<¯–šÆ5… ;µK/ÔuQ¢…
݈ϴ©;Ú
q}#ÖŽ¸$öDRt¸i#Õç˲Ægëšõ»yëØ l6M›ŠéšÝ±ÚY:±Š”96H¯;E—´¹)¦d¯-„6îl²XqUîŒÒÍ™Ju}a(SL-*ÖÜ꺣ËxD¦úèV‰i«›=™ó»ùuºÒmîК@žI7Oæ9-:6±eqtlS%E!¶P†÷2ò…=—Þ'u…0¥f=æ8+«a܆€m66UÆ$§©hפbÐsÁ!‹y–—„ÅÚßThêƒ`ºæ=E¦*2áĘ0k€êî6Œm°Rl8é™Ân,¢Ðtå^n™uÓLG8‘9©¥F²Óµ–9Á”t‡iȪ¸ØºçÚ€ H ³¨i¬›NêvS¬:áàbŠœÊïçÛtÀY ¶
gîYïëÓ,*Õv›±¨MÓ¦q'ejÚÐÞÑ
6šNð±=—"Ëxu ©Ù5m(^ÛÁ0
´$mdŸ.fÓÙ˜æjZ,h»@0¼6, ã£Å±Éi¦cW)Ùùݼ„ÊÙÈ˵CÎuÆMÑ
Õ;¸´fSõÄ"«œÙK6‡Â§”)ꉕJ‰ÒèR‰žGª[V°šŠ«Uä%g/Ì9©ë®ÙnE©TÃ±ë ´¤Ùö÷ó¾žJ}êÉÈ$lë6‚‹¤íd¯N9ÙfÝa
›.W[S„“í¹,î%Ü{-œ“ÐÜcâ9òŰwÄÐ×½Xb–ÝÔòâ&•Ó7%)›†¦CÃR˜UBÑPŒæ×9hzMwÊ&µs}þø5€´j67-v°&¤6Ö9º9ÌNÝ‹jîŒñždɹˆË¬ëµâÜ+ÝRæó›jÃK,E#VÁ|¾tÑ™«#qµX¼INÙñžjñ»yWÎÖe·×Jh›.m¾ÖM±Åž0”ØÞ”v(èÂsÃÒvçØÂv¶žºÛºrÓnméÉRÈñȲê–K»)–—Z «¥‡>縻뱵OutúY>ÞìJ¹ƒ ”Þ‰)GW–¦î‡yëçÎNUPê&ÛÆk]ë1Àyœ„µš†‚–=3¡è¹ØF]c˳¢µûhsȦAwã•T¨”—šc¸Ã`xI×—!Ȭ•!:}‹á<jhtS@ÆJmèáÃ|›\;~A³«ÌÑe²ÃšV›#k»©Í†ŠÕN÷¸±„ö„Üœ©.l`
;ÍIÓaÓÁЫ®&²éÁê&h¼Z6‹ÙlòËJ¥"ën§6…^Âò)†ÅdA>Ìûz_,ëžk+aaz7]:Ÿr±!åÓÕQ,t`:Säe
7ÈÎΦyƸ¹S×À}»Ôã¤J,Ñ–U0b´)$6Äšº-f[T‰FØ5',"/‚lÍoç-æ ™›ªµi'E{ÙÑÅo0m\ËŽ¤6XHwî4.tªÝs7÷èñ*ÑH› Äe¥Rá‰9–4{.ÅJcQ g u®‹X¥J„ȲB¨õ‹Øó•F¶÷E̳“_tÀ/à$"KÚ¬ìµîXLÒ4ÓÂlŽz²rÁµd™C‰Zíî…‹mª˜¬W±iO{Gе®G7•`ã’fQQpµë±Å,žqKé‡ùõ4Tx¦`ˆ"°ŠEjÈfiJ:V"¨U^Ž7"f{*6ÔYÙËÖ‰ÖJÎI ĹSÅDeFðHÄl±i!‡ô\X8
k‚nˆžïç7Õû*³½].”BNØl’ŵ[׿àjvè V–êšB¼#œÇ²FÖÎÒP%;@ŸÓ16ÚðűDtqeÖÖ:-ÆÆPº9Ï=
Hr$jZh»‘â‡yK+²Q¨±vv+A;xAÆ—%j”ºJª%%-'Ñ´Yj‡“ŸŸßTe•üH¨(/¥%qš²Ð …g¦ÅÆF‹¹/
(`\à*$²¡'-B% vªæ-ü<³dÇ…ôù0cS] ©ÕÎÒ”4…¸KîPÜB
Y2;+fk§FœÒÈu¬ˆ»N•"‘b…&½^[ËŸ]%u„•÷|º´üb³'ƒ…Ф,ÖÞh¯R¿›·t¦‘н¨6ŠmZ—(D̦Ø4ô‚ì¢ •Áz»¡p7iÉ‘€—/UÒkY©vM•ÒµP¤R( ©êË¢ +Œ,º÷#Trcƒ|7o›A 'zÄ€téHº¸§œk;´Ö£7ìÈ‹ë¹Z›Z<#+%ŽK ¬
ˆ`K°#¦aØ÷Jhè"” °QJ±
Ik“Òõ%Y+º"%×J¨`ÃEX)~˜w¥×3¼¸!„—"q™-×Îg(6½„¢Þv/w*vaj6*Z‰Ò€¶È÷ó§Ÿ?þðãúÃÏ?üô§>þôùyî½t“
–ЮRKG)ÔJSa
Ò¬xš¨5í
QwSËšZkê~˜·›’vX˜RRq³`‘vaÊN 5G¹2mJ%ÝdsPk¯ûÓÏ?þñO~Þ÷m¨lïççŸ?þðÓ½›i¤mT8¦áE"ù¬Bj¡¸áyyyH‚mMP*XlA—º¨¿›oç«2%ÊšRIm%¡T¨Z¤ÁS*J7©î&9c¤é¦üáÇÏÏê’º±¢1ÜŸ>ýøóîãnš6´n*E
ÖÀÑÅŠ©sv y¹à¤-_X»ŠîÑ0ºç÷ó,©à굕X¶ 7H§D]ײ7UŒ÷óÂe`) hkí§?ýñÓnVÙÙ9(^>~ú¡>X´œh–ºûè¦fë}¹†:«EKSMŸ?I:Ö¶Nž
VÎäèAtÈR«=gÊÔrFh+ÚvŠôZE
¿w¼ÄHue±©¥Á4h"uuŽåZ$ŽÀ3÷…÷tuÙÖ¥û8é^æã~ºR"]¬”*çSH÷íR¨Àó§Y
IhÄ a`ŠYÑu‰B³Šº‹áTH¶ã±iÊœ åûyKžuÃýØÐl¶®ë@7ÒÙ¤´©ž½i™î84×^àIóéd¹N
ÙuúL¢%Ÿ’òÅ!“£5Z³Ìâó§s];²²Ùä¬4¨€;.s?ØÛè(_æêrQ¨ã£›ÜSÜœïæ[î°ûÙ^õŠTiçó%*PÖl¤<§ ˆˆ%)ŒÇB/ÎùÀAX‰³I
E®Ô¦ÆB‰ˆ%žD²ñü´ß@/:{•4*ZPVk16ÜS\,{fc×tJEtñ€)Y~?ïjФ;ÊA«ûàžM³²XÖ‘ÝYÉzFW"íð ö_~<Ô=³6Þ½x«+tzâr†“еäzñRÏû+÷°4u€¶€ÕB¨+VÖkc NlõŒ9HØŒl#‚†ó–é5·×ß'“ÜÃF§ƒ69@ªNpÕ†ìpêP¤à¬½Í^þñ‡Ó@È¢9^kŠëKªV§Š ˆV"]t±zÿ8o: uU\ìNsìÎXÔ¤ð%:´D žÂ=D,d×ïæ[6æ9œÇ€NI=á‹SÆgœjÕ
Še•<¯,¾°êr`pâ|þç›y²‘*V+µ)"ÖJ'`
"mh+ÚºãÂ~þøâN#’{8WM*RoB;5ljY«äyíTA‹|7ßâV
Ï©]¥u©XŽ/»±µœÞeéBiiÇ.ÝÚ”
ô¶þópp7…r§aíB—ZNvú…ìR…µÑš'„öþ—ëAŽë¶.KêfI©”R—ˆT–Ö²+… ÅZOgœ"žÐóÚçcl³¶¥#µ¡kì^^¢¦µŠnÅYë}‰wö‚\'¦¦S‘3Ù ¶µ¦³!xL":â¢i"”N¾øùþÇ{ÈFmÓ+z¤€>MV‹D—Ø,xÚœ™¢tý~ÞzÊÀ,R\ÅcC*GÈ9©;
dÇ
¡¡¢±ëüô‡R¥Í3ÀfëÐéÖ©UÔãš%'`µëæ
´@ÖÕR˜çŸ¾©¥'vŠ4K¤ìè$›¨l,tG
-ƒßÍ·a–I›]›†F7¨€R/WKÜUYµHM›µ´nþðZWtÛjÛÜÏÈ=õ%E©
wZ†Eh %måÆmÌÏ>X6¥M³’],¸
T4ö¸=vÛªµ¶BìbÉòrà»ù§
H7’lwÀ¦žìPpi¥Å£d3p´Dî±t 4VÅ~Rü?$Ákš^gve×9×>_ €J]Jî‚ERåþ·Æþ£$óq¥òF€ "λ—ƒå1ª¦Ê›!‹œ ”j‰7º‹¼‰Å˜E+ífñøòúrÒ¥°¦j•ZLÉb°ëU[]ÏÔ6@Ü´R7=¿ÌÿÕ þI±Så0ÇíJ¶c…mƒÚº4'[kñ|{âþóÚY±.ꎱU¤/cJz”Ô¶¨`åM;ÑXZÞ4÷טÂhÓ25ÀBä@eÛe'"9aǬ¨ÐWmþi~Ⱥ¡éIŒTΈU¹.õ(HÖ2˜€ ÂÕ×?§›Ú Rçt6©•7ì芒J4»6ÝB$KGÓ²ÙOï©‚ÛAŽË
´1M6»ˆÂ*›¬4Ø&Úžôçù±îÔÝ!‹"ÝáÞâGÎhdÅØÐC\°êë§jÉ6"¶E()NjKÓõM ªV¬ATda;”‚öËw# ®…^mÒ…{d¸5{PÏÓT:Òu¨M›%ýãüd/ªÁ¢k ö‚Ë;½€ÚlÂæD\„B;HSÙ|ý«T7V[¯V6®)"&xÒU‹ÍM*Â’ÿüOßxömh•VúåÙ¶:ÜíP%4 ŵJÒs)d+ÖTÖj3ðËüg¥ž¹³AJNØ4#Mk}s3D :ì(ÛÄ|ù»Ú’¥vx‘Їo¥¸ Z ˹(!ü.9
þÓ‡çN'ï¾?¯Š•mc;|~ÆÄš¯2µaÄ¥òz¥#Ц^`-(f™~žÿP!fEVÌrǵ„HÄMèÄ#V¡e°…~û]ÑŠ®&Úž±c×P|³”ToÄkBOªùOÔ†¾{þM$ÓäË;Z9Ëfc‘=¢LÏ“-êIHÙ@Cè¬(4¤Ù?ÍÿlŠ¢«lÜãrú¨i§ÑµB¥'èJl«,Ä*h_þ†#S´„ÿ-[N1¬pn”q/:)jI±ÿî›B7x=}ªyu(–ÈëÜóíãt@CJkCOc\T
!ÇP6î”nýy~À6‡Y±$†TâÖ…MkA;N„suZºü/¬+6›XK"úq͑º©S…Mù÷ Ò’ÜœWß`°Ãt¿<§J(e'Z´¦JŠwØ3YnÓ©(X˜&ûóü˜6Rh^¯Oè^˜ã ºToëè2©]ØÿÒ5@Ãñu§Mc°RÓaqÇ7%a¯BÜXSŸÞ§ÙÒÐW£ï>ÈÂ\Aš—÷uÙ ¿«ÚÁ®¤}5w0AÔ”’#R]þ4?À”ÇÑ@ı’NÙ±ç:™Wd¯é»Ub:ÿ}_!¥¥ÃâuÝêI7ç:
'¢!²„{„Ãwó°$v½âÞÓ—Iâ¦ÏËkеž‰»&jœâfípÚJ@Ô
¥SØéÿ=ÿó£í¨}e@9ɲv°L™ÅôJ ¹F›PV¶ó×[j5Ôª¡iLS§ÝbÒ@ÙBÂÔJ)È›éÁ–©Þ“úuãmÛÅrZ˾ú(§žY B¥ei»³¸Ùœ5û:,ö¦öwÙ—ûbq]ÿ×ü˜¢6õN3µé1öÄv'õp¦ºtÚÅ9òÛgPP²u±Ö TLKS6I±‹%žˆVô~|`¥k²¡.ðÛUGaµˆ‹ñˇ(V˜b×ݰàQ»AšX‹„ Hz=`؈¿ÌŎZwN
iî ÙÕz»Ê¡¯ï‘4§.ü=`AjØQÙiUvy#ëoRFª…¨KòäPk hÚ|¢ÊýØ ‹b~ûHiÎ…m!ñw-‚/;hs,M+¯¦ÖÒ_æÇíÕ,vjsR‹£Öœ×§Ö*ŒMJƒ÷À,ù³®D:4ØÂJ(¨¯.VÄ7+OÏïÏÞ½þî1»¢æ{ÄF…Z~½l XÑÊ’×÷¤¬»â
…T¯¬Ø
jrçDhS
¿ÌêñMw%Æ.”{!ÝiûÔ{jµ#®ÓœÁ|}ºÍßn*9qR
„ÇJ|ÛxòþÃó¾{¾ždŸ®ëÝó‡ß=zã~(Õ®Ú£¹klSdêÉ:ì™±7ñ
6Þ$‘eoÀ7ï±w§5[fwj¿Ì7R‹#ºÈBÉ¡¸ö$»ÚYºåÛ§
¯1ÔÛ݌i·Á^¯Tkçý>N.WZŒž°ò˜×–ç {OÜ~}Õ£WA]Â×¾AÞhk\NcÈÊMªààÆÖbãjÉçGpgÙu#vOΔ¸#8´)ÌÕÍ‹‹Ö3¡v†R 7l슲ƒ›¥ßýáãeLJ)•eƒ49_SÞùæv0_ЮbVØUýöαUêNáØ:Ï4‡“Ò–hSh~ž´³Á’¸]Ï4䜫žLµ’²‘l#vùõ+Ú "u‡”Ü TŠ€åÝÿx—êJLcÐ)’]¿á·È¹v:%„¯_M[R*U´b‘
+IËkJKQ ö~XÒ‰‘’Ú¸Ÿç'ÑÄFdGû:.Ò‘¯—'Uìl¦vC¹ÏµýK6·ÕØJ ©æ%áM1˜Ò÷Z£¼©
¡¹®¯Åoßwc¨Ùvù-¦¦6T¨ˆ@¿}5M± «Ø€tÓ%-¾y€6íZŒ™ÚŸç§ÓîVhW‹þöÜRöÔg–TN wÓS$§zøûÊÎr²ÙŠrw²§ÜÂîéÃ..`ÁÂY¸M»<}²ûòÌÍbw¹ýó®§¶.ÒNj{\h‹e¥Èþö`¡”Õ6a¥k½¿¶æN÷lšÝö—ùÁ7@dO‡¾l²ÏÍÑ&éÔz.¶˜E#7édïˆXƒ(ÕÆŠ6èæÃoLûúA´•¦ÍŽ]Ü9/¸_¿ó¾v _þZAHê›R+‚ìËÇXÉ.oú5vÐUÖc6›î¯d^^ŸË훓_æ'
ihÎ\-ì`] Êž…x:`s?RKþŠlª¦Ý–4Vt%Eaóø×뺵áQ,KlÁ €˜ç×Â~îuÙðå×OA@L K*Vˆ›õ‰6hpIÝ4ßT”A=#$Kâ™ë ÁõOózJé\Ûx›¤a‹{UW¤½v‡æÛoވȮcº¾~}஦–7yþ—ðîëæîËû f-È‘]×H›Ç¹+çóß_¿~úôú’ÔŠ¶Z°²»dóí}§pGÖ-M—ÕžÒ²BµååÑR—
tù¯ùO²´@
õVñZÈÐi0¼1k¥Í©Ã_ODj@Ô'PRùðO”æ7f¹¿‹
J¶IiòßNOe´ÝH)*’ꪥºÊùpG¶ ’²„B↸¯Cº+ö֓Ʀ¿Ìâ2뮈œa±-éRu½4”0éÞŸ§¬–͆º,³èf‚û‡÷Q¹îzúòt!ÔJÙxÒad>ÜK¥ €±@¨H•F°UÁûg¨A‘a“
S7‹v}T˜” ><qóÇùÏ5¢SÍ:/W4Eεjc·ÓP›¿+¬-!TR÷",Á?¼—ƒäùo\çºßƒŠÅÚˆH:=;óô^Wçñ1wd¥)¡ âM-Z˜¥”Úeº°-µÚQ°k´–øÇù±ó