ubuntu-push-0.68+16.04.20160310.2/0000755000015600001650000000000012670364532016453 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/scripts/0000755000015600001650000000000012670364532020142 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/scripts/check_fmt0000755000015600001650000000076312670364255022023 0ustar pbuserpbgroup00000000000000#!/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.py0000755000015600001650000000123512670364255023115 0ustar pbuserpbgroup00000000000000#!/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.sh0000755000015600001650000000011012670364255022510 0ustar pbuserpbgroup00000000000000#!/bin/sh # a very dumb "auth helper", for use in tests echo hello "$@" ubuntu-push-0.68+16.04.20160310.2/scripts/trivial-helper.sh0000755000015600001650000000004012670364255023424 0ustar pbuserpbgroup00000000000000#!/bin/sh set -eu cat < $1 > $2 ubuntu-push-0.68+16.04.20160310.2/scripts/noisy-helper.sh0000755000015600001650000000011412670364255023115 0ustar pbuserpbgroup00000000000000#!/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/goctest0000755000015600001650000000300312670364255021536 0ustar pbuserpbgroup00000000000000#!/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/register0000755000015600001650000000320412670364255021715 0ustar pbuserpbgroup00000000000000#!/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-hook0000755000015600001650000001004112670364255022111 0ustar pbuserpbgroup00000000000000#!/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.sh0000755000015600001650000000101112670364255021427 0ustar pbuserpbgroup00000000000000#!/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/broadcast0000755000015600001650000000405112670364255022034 0ustar pbuserpbgroup00000000000000#!/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/unicast0000755000015600001650000000442512670364255021545 0ustar pbuserpbgroup00000000000000#!/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.sh0000755000015600001650000000002312670364255022737 0ustar pbuserpbgroup00000000000000#!/bin/sh sleep 10 ubuntu-push-0.68+16.04.20160310.2/sampleconfigs/0000755000015600001650000000000012670364532021305 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/sampleconfigs/dev.json0000644000015600001650000000070212670364255022757 0ustar pbuserpbgroup00000000000000{ "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/0000755000015600001650000000000012670364532021324 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/urldispatcher/curldispatcher/0000755000015600001650000000000012670364532024340 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/urldispatcher/curldispatcher/curldispatcher_c.go0000644000015600001650000000243512670364255030213 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000457412670364255027717 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000403412670364255024527 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000001113412670364255025565 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532017761 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/doc.go0000644000015600001650000000133512670364255021061 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532020532 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/api/handlers_test.go0000644000015600001650000010557612670364255023740 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000236612670364255023207 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000003706612670364255022677 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000261612670364255024244 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000001215512670364255022666 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000427512670364255022626 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000377512670364255022316 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532022047 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/acceptance/ssl/0000755000015600001650000000000012670364532022650 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/acceptance/ssl/testing.key0000644000015600001650000000325012670364255025041 0ustar pbuserpbgroup00000000000000-----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/README0000644000015600001650000000034012670364255023527 0ustar pbuserpbgroup00000000000000testing.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.cert0000644000015600001650000000220312670364255025203 0ustar pbuserpbgroup00000000000000-----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/0000755000015600001650000000000012670364532023363 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/acceptance/suites/pingpong.go0000644000015600001650000000644012670364255025541 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000001574112670364255025372 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000002170712670364255025665 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000001321612670364255025050 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000001104712670364255025361 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532022636 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/acceptance/kit/cliloop.go0000644000015600001650000001374712670364255024644 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000257612670364255024643 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000615312670364255023745 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532022612 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/acceptance/cmd/acceptanceclient.go0000644000015600001650000000273612670364255026440 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000001251512670364255025671 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000416712670364255025535 0ustar pbuserpbgroup00000000000000/* 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.sh0000755000015600001650000000055312670364255024501 0ustar pbuserpbgroup00000000000000# 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/0000755000015600001650000000000012670364532021245 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/broker/broker.go0000644000015600001650000001013712670364255023064 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532022536 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/broker/simple/suite_test.go0000644000015600001650000000350312670364255025260 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000001715112670364255024365 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000257412670364255025427 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000001434712670364255023554 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532022722 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/broker/testing/impls.go0000644000015600001650000000521412670364255024401 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000003324512670364255024611 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000354512670364255024130 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000543312670364255024761 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532023276 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/broker/testsuite/suite.go0000644000015600001650000003212112670364255024757 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532021606 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/listener/listener_test.go0000644000015600001650000001633412670364255025032 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000536012670364255023770 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532021444 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/session/tracker_test.go0000644000015600001650000000417012670364255024471 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000414212670364255023431 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000006105712670364255024530 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000001231412670364255023461 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532021115 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/store/inmemory_test.go0000644000015600001650000003321612670364255024351 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000001566112670364255022613 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000664212670364255023651 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000001422012670364255023304 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000212312670364255021755 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000552312670364255023332 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532020537 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/server/dev/server.go0000644000015600001650000000651312670364255022403 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000366112670364255022670 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000237112670364255023021 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532021264 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/launch_helper/helper_output.go0000644000015600001650000001124212670364255024514 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000141612670364255022666 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000002212012670364255023431 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532022210 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/launch_helper/cual/cual.go0000644000015600001650000000474612670364255023500 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000346112670364255023773 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000004172612670364255024505 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000336212670364255023100 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532024072 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/launch_helper/helper_finder/helper_finder_test.go0000644000015600001650000001465512670364255030303 0ustar pbuserpbgroup00000000000000package 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.go0000644000015600001650000000510712670364255027234 0ustar pbuserpbgroup00000000000000package 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.go0000644000015600001650000000611512670364255025556 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000502312670364255024133 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532022530 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/launch_helper/legacy/legacy.go0000644000015600001650000000477112670364255024336 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000720312670364255025366 0ustar pbuserpbgroup00000000000000/* 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/0000755000015600001650000000000012670364532020314 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/protocol/state-diag-session.svg0000644000015600001650000003353412670364255024552 0ustar pbuserpbgroup00000000000000 state_diagram_session State diagram for session stop stop start1 start1 start2 start2 start1->start2 Read wire version start3 start3 start2->start3 Read CONNECT loop loop start3->loop Write CONNACK ping ping loop->ping Elapsed ping interval broadcast broadcast loop->broadcast Receive broadcast request conn_broken conn_broken loop->conn_broken Receive connbroken request conn_warn conn_warn loop->conn_warn Receive connwarn request pong_wait pong_wait ping->pong_wait Write PING ack_wait ack_wait broadcast->ack_wait Write BROADCAST [fits one wire msg] split_broadcast split_broadcast broadcast->split_broadcast BROADCAST does not fit one wire msg pong_wait->stop Elapsed exhange timeout pong_wait->loop Read PONG ack_wait->stop Elapsed exhange timeout ack_wait->loop Read ACK split_broadcast->loop All split msgs written split_ack_wait split_ack_wait split_broadcast->split_ack_wait Write split BROADCAST split_ack_wait->stop Elapsed exhange timeout split_ack_wait->split_broadcast Read ACK conn_broken->stop Write CONNBROKEN conn_warn->loop Write CONNWARN ubuntu-push-0.68+16.04.20160310.2/protocol/messages_test.go0000644000015600001650000001254612670364255023523 0ustar pbuserpbgroup00000000000000/* 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.gv0000644000015600001650000000143012670364255024150 0ustar pbuserpbgroup00000000000000digraph 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.go0000644000015600001650000001410712670364255023550 0ustar pbuserpbgroup00000000000000/* 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.svg0000644000015600001650000002031512670364255024336 0ustar pbuserpbgroup00000000000000 state_diagram_client State diagram for client pingTimeout pingTimeout connBroken connBroken start1 start1 start2 start2 start1->start2 Write wire version start3 start3 start2->start3 Write CONNECT loop loop start3->loop Read CONNACK loop->pingTimeout Elapsed ping interval + exchange interval loop->connBroken Read CONNBROKEN pong pong loop->pong Read PING broadcast broadcast loop->broadcast Read BROADCAST warn warn loop->warn Read CONNWARN pong->loop Write PONG broadcast->loop Write ACK warn->loop ubuntu-push-0.68+16.04.20160310.2/protocol/messages.go0000644000015600001650000001235612670364255022463 0ustar pbuserpbgroup00000000000000/* 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.go0000644000015600001650000000572212670364255022514 0ustar pbuserpbgroup00000000000000/* 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.gv0000644000015600001650000000273412670364255024365 0ustar pbuserpbgroup00000000000000digraph 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/0000755000015600001650000000000012670364532017403 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/docs/highlevel.txt0000644000015600001650000000675412670364255022131 0ustar pbuserpbgroup00000000000000Ubuntu 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.txt0000644000015600001650000002200112670364255021570 0ustar pbuserpbgroup00000000000000Application 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.txt0000644000015600001650000001756612670364255022016 0ustar pbuserpbgroup00000000000000Ubuntu 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.txt0000644000015600001650000000505212670364255022632 0ustar pbuserpbgroup00000000000000Let'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/0000755000015600001650000000000012670364532022312 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/docs/example-client/push-example.png0000644000015600001650000005750412670364255025445 0ustar pbuserpbgroup00000000000000‰PNG  IHDRk¬XT pHYs × ×B(›xtIMEÞ 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&»S DRl±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”M6PA­c´©Ä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¶=v u€´À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±eq tlS%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 «¥‡>縭»ë±µOu tú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¹2m J­%Ý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ë}¹†:«EKS MŸ?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;5 ljY«ä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¬A­Tda;”‚öË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*9q­R „Ç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–TNwÓ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_þZA­Hê›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´–øÇù±ó±ÄZÏç“pLc°6zÏ¿WƦZÎ_ºñÞ´6¶ä„tS׸rùÐàŽ•ž(×g V*=¶‚P1–û"dŽŸoæîµTœ…Tjòñ©Me#¾ýÍ’ÙwÈ6Y¾}e»CZÉi›l-¶ADW¤Ùdh7I§rõËYÛ2œË7¥ÚÈÿ&ý¯ùMs‚M{šÒÑZƒH‡ 1°nޤÿ81­ÓH謫ÁEÿPÅÃîßV Í+ËðFš P²è¾<¥a8VÁù!V$°)œi–À®ïFªi8¸û uCcŠ]EW¿2-k<þi~jHSwVb•T\Ê`B½ÍÙpœÃXö³§­w\,°¡VÈûw²[øË(„¸4VsP ;EM[|°i oÒëˆP¦Ä ÊÔb›œó‡9’2ÈàÔzDm½qÎ`„ô—ù±J…Ôs‰Kv•xb‘¸vhyù E¡´¸ØìÎùÃu"˜e?-is¤– i¡*ZX¨±çéBk;î§o˦$( µtò.±x ¥–"*–RC-•] ¡ü2?X9Ä쵆½<£-iVÖ•cÓÍøíEA‹KÚN°"¸×yú˜CSl#ç¯î€&ûF+¤®-ŠìëÇV+–Ç?¤ʲ¬vÃÊÓÓbí•V…ÔR[ɾΩÔT!„Pùe~lÂ^ ·t3%ବ{I % íoÙÒ5²sAØóñÑ1‹$ E)¾)x(*e›×7¸¹dÁ”*Ü`ª(`,{QðzJŵp1'¯×H ÅT+b íÏóSØš®2JÁVÉb1Ý Z9CÄ©þ£ÖN…i Hòo€«…NÏ¯Š `YÌQ¬nL$ÖÉëû[èØå«XÛÔ´MÅà3Ö%ÔMªÇ£×>¬Z ¶) ¥g~™V³ÓVkÒΦYŠM ”R»Ö¥ý,*`+îNPÉ{Z‡Š¡?]NxãïRKª®% ³©­ZÉ;KPÚ½®OX¡*¤„ó±•7ZÑ6Ú«_Ô5 ­ ª9¿Ì+Nmƒñ˜ÜÙ{åwnNHÏýðõ+9ÙÚå´£˜ÖÒx÷HÍK¶Vüû:#«µÝ6 ÈïL•*¼|Ä,âxÝZRâRO´î ìÇ%nö~¬P°pOÊ_cA~פñ—ù!íØ(„0¥Ó3ÜO=®E­Äî”2©Ÿn›´Öµ«žwZoŸ,SkàóËœQN¶ Eh´¸áÛð&°4¼écÛ²ƒçE¬*Ÿãâ*MS­"X<•º¤Rm)œ+(E‘;KQw „TªˆÒP“s¿ˆ¼±i¯à‡}ž~x÷áCp{!4ÿ^SZâ›ãôdK„{@*).Å\Vk|)`K…Øå¾uyŒâÙAºTv±oÒíz²ËR¡~B 5lÚÎΦ^EÓ*J;Ô’¿$¬Êïü÷÷Íã©Û<=E¾<_ìµûk*;À që’VX‚šÑ’Z^h¿ºßÃÊ›&Ìç•Ô¢ë¶}É4÷ÓÝôLl5¦Ý©®–P9¦¾ÁÅóÿÌOKv–žM#'nÉê"ˆ{OåÌŠ”æóÊ"'áÿ°Z¶QÞYxùˆS„Ï_A¤5P0Zçn°…ZÔ’Íâcî*¦ŠÎ§Ðl^#j³ ©pž¾0¡W½ÅmÙ¤‹´FPŠ‹¦Áþq~@Zér蘦¸a¡c­°S_c>·bS­î¿^mÊëä™§Ïf³Uš¯¯WÚÈw]"VÞ覯—Zwš ­™«à ¿l6€ hYF»Ýkž‹h±,‘2}}gCØ; Í‚œFøe~ò–7æ~`)-¡ƒÃë D¤“ίKÑ[iòá;Vd‚îvºÝýÎz’ó›…¾Æk!ÛÜb¹½X%·lÕëMƒ°úz—˜b2‹fã0ÏÚP;%Ò¤%¥Ú^숸AðÇù±#Ì™b fgÉ¡ Ò’VÙëOTÛý·º›.PÔ½>‘¾o äWA½8Æ€Ö(Z»6!WÉm²sgì<3õ¹x}=‘ -½h­¶÷”Vv#©@v¤ÙY÷H¥5 øËü€(%`y}tØ\ ž’Îê&Xð×Y©€òôþŒ%Xæ^yÌÊ‘ÏÒò»VvmÈdAVÃÆ¶HIýúò»o_^¾~½__mí·ñMX6YJÚy–•dvÙ3°fjÝûj hYÛÔüq~,Bž‚áj-’bÙõbƒ;-¸š~BµÈwOî,RºqÅ—ÃywaçôK7e(àÆ<ÂZ¦È°›­Æòòå¾_îûþÆ}¾qUJUHhªmpZû¡±ÐBh;WA^LuñÛcâË4Kî'~ž".¹MOSÚPÏrr»3H65'(žéùìÐóü€’E H¶_ái€3û™TER‚¥ |½jw‡kKÐR-*oÖ "T‡Cz¯çõ±Ì’•èǬSÜé­+Ü™ix*ȲQÙ»“’ýuþs³ ’± “”s2(í¦ê¶±ñ“R«ž®ì µÑã›—öñn‡ào’Õì" )-ÃõôîÝõxz<®çÇãñîzzšÇõôîzšw×<O×õxzº®kyºÞ<]O×ÌÓ#w™+×u=æš<}ßkæqåòzº&×\¹FÇëš>™‡×Ìuù¸fr=ñt%×ÏóYcç´v7†)}ôX®«¯ccŒÛ˜µ;ŸXÄÐÇSÓ®UÙ¤öÛÍ>=ž¯¹3—O6Ë-²²}}`)ó ¬ QÀS+rg SQ‚®ð_ó#àÉ}•hوßÙ¬(ŽœG>ÅŠdî°e´¤Ë§…qðËÌo‹¤òšÜä5Û‰ÝôÁصa ; [l¤¹¾<½›÷ï¾{~~zÿ|¿ž‹´KBÓp®m 1ÕÅÍà+tR‘ /ԠŸæ?×V›ä®4Õ“×)H¥©2¤õNîÏÏÊo- )vûÆÀñÞÄ×ù|o®w(—Å­kŒR7á"-Ù²RÅ ûü„½/°_ÎðF Í½IÛpãõ.e£PR Å7–ÈÉÉ"Ú^#w(øÇùAhNÄ"­T{GÛ’óèÎ;ª_M]Æ—‘tçÎeKÿýçËC£/7MÂï†÷\¡FƒgÐMÕT öýÌÒ˜Zö[…:¤˜i¶vyú®¡–ZOÖL)h»{¡Qhl©Yeÿ4?¥©KŠ{ÕMÚdq§¬«Ç9Jj–´ú)j _¾k ¯-¯y|ÏJ¯º_£­½‡V»¯²yXßP+LI×…Â?¥kPÛÌ?ª*eŠ• i#"­±* ‘Ê^¬«°«NvùÓüHnv£{†;›Æ–iºÃ}Ñ›dèë- ÷U¾|ç Íùï{<ÿ¢%ìì|Æ ›¨Àv¦q€”Š6xB³T;æ¾¾w'›õ0ì§Wò’Iò¦”PŸž„MÍ·^ Ûœ´¬¤´g\p~™À”ÐT‰â ÙˆSÒPݼ¾ÖÕæ Mé§û;؈ŸþÖpþù»­Ú|m jÓÄã5›ReÑ•q/¤Î»wU E÷þ ö²°­­ˆøálÜ¸æ ¦tÓ^¸™•µSYþ?šàEA°êʲëœk߈È Rý]ôÿ_ä wwU!A>"îÙ˺=FbðãüàÎ= âvÖ aMm PݘB¨¬äô—ÇmnJKö×çç>ùõ—Å÷·7Å¡;¡µ\HzB㦴9iUø0±ºs´~yÑ®.dãÝ´v*ZÏWv­n*¼\¸J4RÝb" Œ{Œüq~Ø!gJÏT;[é”Hñ%XÚx[‰6÷KkíÒÖóüüå>áÕÓšæFÏâ| Ú&Äcð‘²*÷n¤¢â¦| iÊðñ%]Më6½I±Pø0@`”2h©Ù“¶0E1Àé P›篞l÷0ákjÛ“:*šíÕQÖC~s‡,µ³Ay Ö¦¾ÛÚÂlž_BϵÞ)l¨WT¸ Wš%„W–æý›.n ÿ€š*!©àK6@>¬J™,TÀ”Tìž…DT”Ÿæ»3»][¶°"PEY ´@k[pùuå=)'”Ci9osv]®nÓ4¬¦4ØR°º²YŽ÷ì+ÒºÐ~ùXlq­eå³T˜§Írd«Pª]è–vØÒR9ËÖr~š¬œhSœž1 q7ÔW•’‚Vê®óiÝ,›³© ©Ü„ÌÄ4‚âõr3Hf¯á\E°iªTb˜~õ4®Åô·b °A×7o«&¦iƒÖF|Õ»WÕÅ€Òš`Ö­]6'¬ÕB[Sl÷0«"Ö&Òôy†Ü“ÖªÔ†F°}ï*¤}ú-Ô:J3ì¨,¸IU*êu«ØdñK¥Òˆ`(|«@Ó ¸ÎºòŠE’YQÒM K¨òÓ|OŠÓî$d )V{Ìz¸*¯ªKíFðKÀÍ%‡F Õu½ß4Ùh¼°º'P:Wv§Âµqï´RD°²˜¢Ù‚%¿=§¤;ŽlP6RûÍeÒÝIÍNQË¬ä” E ©0²Mûó|¿…€Rëw¶ÆÖ„²¹Wðr&œ Ž 1¨­¾íRènZž>­ÝQ(ŒM X(CaYky÷¡¾j<ƒÍ?ö˜m,½çã5åUjÉ7G%¡åwY¡i‘ž+Öl²ò/?Ï_`¨!Û‹¬©tw&l‘mî}ôÂâõù$´Ïí¦4…Jw6÷[¬«CÃÛà¦È^Wj­¶FôyjEÐ:<.H›Ö|ü-Ò¬E°T^ùôċܳ®‚› pr_‡kiNhkÊKÀšñ§ùnûåDlsÝq/8Á)ä46¸¼„œk²›²Í}Êà¯OA Åà©Z,‹ÊÓG§«« …†dqy¸£ÐtÍüÉbܪþYhP¶(›JPáéñž4OìæH‘˜&kí´uØtê"bñÇù‹×©uîÖáΈäÎ¥ì&½ÚætvŸÖæË›´ –3Pc_ê9o‚IÝRO—Ï£]a,T³®mD±%%ã7éÄÒËzäüB ”&PCü’;XøÃH´Ý¤gTܨü.M¡Æ“åÄšPºéÏóדÔ#k&îæ½>SH§®Æû¢§={@æ×`/7ò*HƒM}pE¶¼{úaç¡Å²vlWüÍÙ@Vç© È¿ØHí¥æÃ*uã-ÖM ­u59Ø4‚Ö®Cþc¾#fÇrpwœ;rÝŽûÇH=™Ý¼ÒÜôl"éùÒ¸wdŽ*RiÔ/ïdAÇÊ<}B2iqÐ`›ÅBÝ7ße‘Júÿšª¥®,I¡;ïÇ5ÍP+ÐØìðju]·õÕCª•þ<ßïÐMÄWMö"íu†e/‘¼ÜW-ðüÁö*X²ÿI ›ÒI¬Ž/ÁU÷ÝŸŽ C×sâÇ‚˜“qõx’´‚›§§5CÅs­î=qdï±!e·îl€n:÷^?Îwv²ÕÐ@­ËÑÓ¹¯^k$m­‹+–uõ“"µÚt«Ì¸B±1_Þ…(wÂÿŒ¡Owj¤Tå\'M÷«?>’6åè™ú_R@ÜÐt¥w/Vt›õ^'p0³À=)a•Õ2ptÙ I]Çüm~ x‰â‰v¦±—6çå‚a­¯ÂÃ?#EB§ˆV-uyy¢¾*¦ýüOsñþë·á`i„C _}óT…Æ’lŠ]ÿÿ‡†ÈfHº½ÎÌv|ö ¨1{±1ÖVC)Š‚XÂáâçù+‚d6¤òÊ;y‰ÐB¥ŽTrDkZKÏ]ï¤TT6¼ÐˆŽÁ–yùÅ\úmòøþÍSvmẞ޾ÿæí5Ë×2½¯É¾ü,‡à+Ôº Ä÷än:ÀÁ6éJn,©ØÐÝ *TmS~œŠ5wDð¤"¯Š7ÙAC ÖZ±‰ćߴ)PfÒ€%ûÌCNÓmúéKÆû›‹ÚÌÓû_}õþý‡w_=^WåB‡pF÷¿?m°Øƒtø—4U[Ÿ§ý# ÷Ugç 6%Ò]Њ6ÔVQš¿/ÃZ”&µ±#—$ë†r§6¦¥÷6H–,•WÖ °i€<_\ÈÎ?â•ë[DêÂD¨i^~ýøßŸûíãçýôÙÑ"UäTl*¯;ñÃcÃéRu;(M¬{Og/*¦UЭÛÌÌ_il/¶J£‚•¶Ä;05 úð™ZÑ£¼ U0p õÓÃCæ—»C¿‰ÐJN`³9Q–l`Ùÿüç©zž_ {RPñòUe7®Šûö¡¥ "a ©eO`Ô¦‰`§ˆEzPÂÏóW@§4Û{·¡µ"MiZÁ3Ýì–ÊÃ磛JØÔ’bŠùó…'˜|þUz}À„¦C e%nvÈ?~Ù jÜü–¡ãË¥-¶ÍZQ°ûþ 2- %æ5ØPPZ[7↜ttøã|Wê ÈŽ[ËÎ=¼šU»¡Ùv á•Ýø*%{}ag]¬Š­`ݬn`¶¾¼¼þ«<.2'²6•¶©¤üç‹B™´¤ØÇœ‘F æ$Þ©Õo-CZmºiÓT7 E¬aÖ6e ͦˆ?Í÷D‚œͪ;̆5Û0 ÙJe•êÎõ©sG‘Tªˆ†‚ÕÒûËÛüú/Þ¾;z¢òÊW;à´ÿzÞØ7øæýû÷_?œ;(¡ˆEÄ¢¼{c‘lÀæÞˆàÎiððJ©uVak/ÐöÇùÞÛ4eŽl 2'YwÄÜLhŵÙÙŠhK¯ËT ²¢ÔÍbÅ[¥¿òKs•?‰¯²Ldœ_¾üö}ð×Û§Ïr+vHcSØl*ß–ˆÈ±2ºî@šÖ’¬Ø*ÍÊïìà}§‘Ÿç‡š–L¡æ» 4e³élnŒ¨÷N7­¸a¥%P^ –—ñJß=µ½G)±­û@Ý õþGíã·Šl€ëÝg eºJ‰ç%˜Fýê鸮”šˆB¡¢bÝ0]ÌÎЗô§ù.M+vatYaÓJÙÊag—–Š«mîχ»²}úØuµõV¡áPhû9Y-Ø-×ú ¦ÛP–Cz¨v»ù¯ó'K?ÞéòöWk÷d![Í…ÒÛí;x6¼jv³Õ“›,–"œÐÅn)k«èó«°g„BÀJ±ŒJH·×†–(ò2(é³Z•´ÒË ÍˆçèÖô¡<½i\²ÑÍ…ÃâÆ<ÿZúoÔ͈gJÈgXG±xš~›J5覙cJ•Òv*X=Q ¸¡©œï lÔŠâí^kÑJ=ÍFì²åÂR³nz¡Yhí¦J—±™áþHF;‡ñnd3àžO·¼}½JÊõ „EW.°ÚíÛJQSÒ°¢›µÙè.L) Šâj›R?Íw›BSB¨v8ØiÝi¥ÁîZ=4ž>º—æ_’ÖW@…X“¼|ü²—+áÚ²ÌrÔß¾¾ÒÒÙ@µÏ™&‹;”þ¹'%–RêÊ«l,RÄl Î^Rž' ®èOóC°Úƺ6³kŠÜƒÞŶæ…4ìV$Þ /b/\AÀëDA¸G™i³ÎýÛçqñ˜‹ˆ½î/Ÿ>oüڵܣ‘šÞÁæ,ÖBk(ûí `›eÐUÚPVa›9–Œ¸¢çj»ŸïÛ òò vÁÿ~u¯ºšÔ)g môHpž>ÕÊéвƒ÷ˆ“Ö&#õL-Ýóéão_^Îý²|9çùË—ß~ùøüé y¸Vœ4§¤E;£wP¤ÓN¼Å7ïÑh–Mi÷j³QÖ¬Edµb ¥ZBŠä?æûrr?@gzÒeÖ'MÓlÈÊ«e‡mµ5¸Ùæ÷Ó'4\_©@]{‹a!Le;µ g`_ž¿|ùüùËçO/Ï/«÷#k>4õ”B ©þ÷åq“Ãl»âŸdÝEW³ ½ÖjË2j6ZѦ(Zþ>ßß) ê–œrn¥¤œ8Зq“µÔ|÷b^9—íµhÖ&²"èÊC­‚|…ÚT,îW¼XÈþã vùÂ()'’œç›¢Æë ÕÌa‡A{b¬* ¥<ùæÿ~1'Ü KupCDkÐda ¦œÎù¹×áŒ`=ã«6ǵdU^-¯<®de²}ïÜ“Ø9yÔ&/'6•Bv8r’?¿ñhÃï*J8Ó†uÐ"œ¤ÑÍ U›þ4íŠg–aVrã7í€r´(I•^ˆT Á—yúT¦Ù)Ò(ÈÙ „nZ›3X©ÙñØ‘§8=!EøõXÙ ¯bÃã×k‰pÀ…P7¶Økí2i-¯œ®+¥ ¯Âްü<ß§ÓÛcôdöáÐ\î”jÝ4.ÙT†Wž ›òî£x^…j+½Z/h‹n"Aˆ p¿I*ÒðÎô¨(De3ü ]÷dñ\Ücë íZ6uIÅbjÒôÇùìPÉ Ö3ÍήWã9Âê6­ˆiθVB }úTIÃ<©„ÂÚ+t¥)¥ 7±•¹ß jEî‡â݆n²`Ù ÌŸyH„´÷\ˆöbBG¢Zî”RCÖ@ìúó|¿i©–vPн˜åw#¯"õŽ%-i 9öº>5Q¢¶q± úÐZ$¨®nJÓâ1¬|~§G7íEóùÃÆARNDàÏÜÅ 1·U©‰'!.ÒÊ!ÀîŒJãŠiÃjÿ6ÿ®½JD–iuOóëšÃ)˜6ÝKÏѫMØÓA¯‡/²9C%whÖ¶ç‘Zw¨a?= …¾hδ dÃRýü¦)Pžÿg`ÎZÀít?_º`Úe¨%@{kiܦЈëÜŠ‚6à ævY*?Î_Û°tõÌ’ŽÉ2: Äzmé1ëæØ¦=%…Ì—^M)ÐÆ•käU?=ñ»K@_.‘Ï#ŠMm^øüxÁ+¿ý2Ñ*Õ`áÛGØVÔº ed“lÀ”@³ŒU ËÈNAm~ž¿¨Ø¦z.ΙyɆÅNy1ʺh H*PÒÆÉKA^Ií4¢§c­²Ô·^HC«xÃ= Ÿ>uäå·ÿ~¶i añÏœn ¤t!.rÇénvƒ…,t³YªÔ H‚§ýùÁV©±dÓ­š„ûr¯XѦ³0 ký݆ì‚<>}$¬4›Â†Î•#õº$Û@}¹„ô6ÛÉHçOÿüõ×ÏÏ)˜,;ôßb™ÄîÑ$–3”ÄŽ­g¨¨dgmCMUÊF~ž×àFN¬J˜ÊË^gêò»R·”RËyh¬›;é»ÏnºVDööÑ#j¡÷(ÔéÕ¦+.M`"ͦؔ  UÜKΟ;uIȤhªhJÁ/2ÍT^â=(·aÉßæûМauÇÕ WÇÞChÎt ;n*÷`zO3'ÚjvØáé×Ái œkç!•Ê«šZ„€[ìó· ²X-½–U^ÕlÜr_TéßFª¶¶“,Š\ôÌašîÔN´m6ƒC‘›9 šŸç{NÚЦœk]j¡†Ó`è&ˆÍ‘h[‘½Z¡]º—ùði-Y4·}ÜM¤ ŠW=ÛÄ ¦©°’BÑ{j_®¸ßÅ*R…ºQ@@aÐZÜÖj7®-¶fx5Zÿ6ßÅt”3G·ã*if°TÕMŒi#µØthЦSèû}vÕm“È+Â" GÝpÞÔØ¦Hgùÿu÷2܉•´KÎ7()‚]¤ P õU­Q!ºX•*5¾ê)ã–á§ùº³ƒm‚ŽÙPKsj]ÚììÍ µÂK‚|N«='ˆõOóÛØåZºÈ‰H µVÚœ‰"¯jjÚé`ítˆ8i媾}»mŠD‘ØÈ`»Þ:¸®Tm“Rèx¦Q6.HêOó"`ÝÝJÖˆ6Ûë¨Í ¸ÉêÉ3cV½š•œ´œùðeUdfË6 -5õÄêF ;[âÈjU8j¡R¹‡ãÓ·Ó¤DBXÑJî $Î* ÍÞC¬!ûòX¬„†Öµ?Í÷ˆMË,´Nz+Š¿CsšFªeësRKDßòeª3ES´6ˆ†º5nÚ‘X9ŽhË^Vƒ+U úǯx --±5ÛtpE–†/hÙèµ´ÊšÛ®D’çÿ²*‹lvÛRhš“ã=»,œìñ”’³e©»†sq„—>”úðø\÷êz"­«åP°dÝs']»¶i]Û®ËY <°Š [ËÆn…v‰•t—E–»ÔµPéRë=‹¨Ë–ÿ˜êZ¤…#;­ëz’]ãÅÐW‰˜ÛÓÐ9Ò¤ZHM¥Ólô]ž§S!n•´dÅZZpZí´èó˃ ¤aP0ÀòÍ75«R¦˜VÀµš°(’%†‚@°˜ TÄôïóý@ÔV ÓãHèæVÓ,´Ä´¤w¶Vì¶é”Ϊ¬G ïÙΦÝPÀ‚©VyU©bÓ"\Õ.e,"¸¶_û åUpeI#`Ñ•T«© `)ô¤0Øs…67\-á„¿Í_¨¸¡†Š5xè™u+ÍFïœ ¢˜j³tR´àéV÷z{“ mGQðt¨q$Ç$¡:Xh]µé›?½-/’ ÎìˆÓ Ù*ر¢@mJ0¸ÕN+)÷d$ÍOóƒb\wÀ¶4C§ZÉîNSKxuÌ‘ØodŠDÓžÇ"^;oÏ$²‚ÄBò/‘ø"4"±ah$.}øæ}›ªÍžde,­‚Û„ÊÖ«œâÚµ¦4k9ÃNYÿy)E“Êþ4ßAQîÓt؉¬­»b¨0”º-JÍÉÆ¢'vAJ‡îuúX¹òæí9:A6R²}¾€nšB”Hcŵ;€¼ûã»k#‘žËé ì¤>_6¶ZÍ0(¶q`T65p_æ$'°ì>ÄŸæßãpše︛M' µ¦çR‚XtÔͶ/ÄM“é”yzç3ØX•¢„A¹8ŠÚ7xF|yxÿLJZ´GQ¶³‰ÈN3°QÓ@)X¶M+M:­¥Åaûãü°ŒqqC²Nõ4/¸Á²!'mŽRÎÕH­îdóTRNZO v/Þ?ž—[9Ri‘n•ÀJBÁ2ÔÇo?<CŠ ñN‡m+ØÔWÏ`×´„.»ÓÁ=—5H¨%À;á§ù‹Èr¶•ÝëvÓŽ@³K”¸lJöÌn¨ 6h›£QEîœ$óöƒ¾ ««IÃz.‡6«¼ê@ûôõ7ï²,¬4Tƒ¯*!ݹ'%] &û0›m^Óµz@à(*ô%4?Îr_ñha6!¥Ù°ÜS ã¢„…smì^ëX@Då_N\e.PÚñÕÒ\o¿™Üˆ-Äßµ‘W ­H¯7ß|ýF¨Ô’ÞñUŸGí1Èb×Ý<)¬¯À®XÌÖs‚Ú*ƒöçù~”^‹H8³w‚¦tNˆP-šZKÉZJÓtW=˜ZÖ€ Ý ÷\o>|xÈl+ÕkQ6tÉÃÛ¯>¼ê¨È¦¢cvaÇâšöe‡Kz•Â(4 køÝIêK°X^û÷ù.¤Elã ¢X­¤œŠèNÅlª{‚x¦ZCiª˜Ôb# bñ~p×yzûáÝÛLJ벼Lçááñíû¯¾ùð6—‹l ¶·L{A +`µ@VLVª›ƒ‹)}±±rAØX—€›ïϳ;”#,9Ú²é4tãêm“iDzHÚ´6íιLëk̽LL[…“yxxûôö«·þêÍÞ}ýöýÛLJ+G"Øà{ª•B´÷UÖf-¥C‰é¤¡, äàAïxj;´n<¬n°R\Îßç/fÕ+ƒhŽØiΘ{-5¦)œ Fa8fÓ#Û‹YOd0¥Ӧ鯴“Ö=³žòx˜ç«jÊŠki@Üÿ¯;8Úµìʲ*Úû˜û„ÓüiGðÿY¯Ø×%ñ*Û÷ì5'L¥t¥@<[´&FÚïFp´Ýqí h³‚R«g‚ºQ§EÈž,b•ÊèÛ|i­¡ÖMÁº¹Óa!Ä Éb‰Í1žÜ"4’5AÙnúÈÎΌ۬8¥Øƒn%„3³¹‰µ*BIh,wf=YewÚrœ%¦ÒѺ(2¹+ ¤–„ÐXù*Hßæ³ôH,LiÁ&៦7{¶ÖY4– µf€Õ…).”Å0¥zé™BŽŠ’j΄êÁUÄ*  •˜¶^¶c@š*Ó­ë*=.iˆkÕêbÃN;hÝ1µõm¾PCl»\K%µB«…J7B¸ºÀ,Áâ¶nT̆†Å‚gzÙl¬užÑÍIu‡*ÖZ‹˜ÞW=©ç<„B<Ùº¡¡µlEX(AmîÉXÅ€Äm,6$•â”¶›¾Í—;Kü]ãnÖ°¬MMERµ9Bqg;²fw¯ç˜+Øjêã“2; k¹eCÖ¦­‚€`÷¢„t£kÖ½6•RS†t^›%iV”š¦%„jWSm1G—6”@7’ú6ŸÝà¹È¦«©Ï`Ál¯û\PïkÍÒ÷>nÌ¡½“Ý4›LiªÂ^è9 „Í‘”‰ìÐUø_^ånZÁÆcn­Ns:X]V)±ÖNUž ljEŽ ÐT¬4)‘Ðâ"ÊOóÙPS“{NØ"A]CQÚt†ŠI›Meg†y¿Ò®"±öÑØºµØTlSt!çB^*MxyÐd6Rš4žBêûøÛ]ƒCt\¦ùýÚ9DÂöêjY*¶Ò2.t›Û`¬Â/ó…u¶{mÍ©inì™Ú K×6]Á®§WM÷\³Af%À$Ï^E˜ûºO-Y²à€VÛЈr”—Ë%³ýäyÀR×z‘Zd‘^ f¹‰¡¾65tá±5ÜW!?Ïz^¬DvM n ™­¦€Hp‡ V;étÍ…¬ …xíÌjÛóßÏ#w°œÌfi`%ð¼zšÚ W­¼„÷Òˆ¢žlE='¼¸iz˜´0PêfmA 6ÒÚ* ¶ýy¾Ì*C÷J¿h­õKÓ¸@ÑÜ&Pif³¤»RkƒéÝ´&~ú§:Õ>/›Bv¼ ›;̱Â$aKgÅæLÁS´ìÝ„­ˆØÐXs0H64¢@¤Z¨¤ž‰çj¨–Õ0ùeþZa6Ìî<Þ§qM³ ”ÁÛ9sî{ìì¼g,93‹T*Ùï¶aè¼"zƒº3‹ˆm¦1Ý«œ’˜U@߸;À*S–Ð;å6Øé†mñ…Õ¬T…¦Ò9S6Ít¤;Џ$Êþ4_ØØŠ ©wxz8•cuó¼`û K6÷=Öz§Ûܱv¯ÅÅÓé£gíiOÓÍ®,° ,VN+¤…š’µ,xlõ€Çîx «šåZ˜Zz«[ÎÑtC…;µÐ\°œ³¶‹Û£oóc¯¥vZi(ÎNC“iŽn£wî±8Ž›ý„9YÍUH“EΉ^4î¹ÝÐh( (´{-'¡±ædAo8—v!®T¤¦bÉhÓ*žf' ‘6£Le‡ky›ÏrÔNä÷ûa‹=;Ç‚ !‹ÛªaiâQΕšÚE˜¨¹Ãf+%w„´Ú'b¨q7gØ$Ç\”$’•^ŠDM—a‡Âû£ÌRjw²é™{º¥ˆƒí”fž/˜•ÛeQ|ÐY¦ÑÚm§Kº—;‰Ç=µŠÇÜYY›u×Òéav8ŒˆbÊKê=* ˜tݘåTÒxPÔ­Üöll”íN{I•¬ã½÷µÙu$­›l–¥uyÙWýyþ*ºç’.RÍBK§ã`s¦Ì­&5C𵨡MG´™Í ‘°Km°‹/7L;¬‡¡mÌAìslÙxfÁL“Ðduki*í`3JP0ˆø²qVÄšú6Ÿq3 $õ$è†ìs ;À>´«*%*aÓìP‚—H åÌÁ+nÜY«c‹ÌaÒÔ)æ®x\¦¢î”X^rƒwéÂUVK©Úð²–Rªä =´©g´"›­¢¸½ó6_º¦ÚÐ÷¤Tñª›²&T±[Þ¯®d1œˆ$%Gݦ“RSE^Öl© å=áHì@Aa5K%XvD°::~u^×4*Ô`#JÙGQZëÉ ±PB„Ìu~™›žnšˆ â=¿ÖEåÚ”‹]¸¥Ÿ÷*gúîŽ]¾³Rôymžµ4p†ÚÉa‰‚GàL_ ]¡¨m£ÁjÙœ½ØF0-DΔ-4<DzãÒâ .õ%ټͰCcgˆP¼9bÛ¸ÓÚ Œ¡`µa“<5Øi8q5J³›{´x¦± a‡œP6ÏJDp±+ØÅd Ø ±%J:Ñ£íH@låÄ ï;Eå¥ÀOó¥MÚì¬C< v!]ϸÀZL¡{fgm9Ù{\L'«Ï¸–4»É1H:ÕûҦع¶",AÒ`ˆàóºD,P X)’º ¥k  V°õ¶ØXAN™X•Å4¼ùe~9PI b±¹çÄLe ÚkÁͦŠ}¿Ø„Ž€æºˆxGÏÞIÇÝ´S !P ØvêJ÷äöÅ—™s"(8»Bš3»Ú {‚‰ky¹ p¡ÁZ«C«Ýî ÛfÅsBÓ·ù«¾O„µ4'Ð[ˆ-;íÔ¢¹Å»+uKÓî > 4£Ûy>˜£!¹ñNÁ§1ݰFKÀZT6¸u“Rè¶W"Èß¿Š]R [§YµV!‡C¸'YAÓ ¦¸ùy¾xf7Í=†Ýœ–xLÊ–é>T°"‚vÇR m²ÂJêCW¡Kµ± ÑƒÐ€UdÕ…Æj¤JÜÜÂH1œAÐÂþå±a–ÁîÙ”¤¬ ¾\Ùó¨Ïhñ¾ 1Ê/óåŒáetÓ•Z"»å²ºZc—FCÉ5Øôrݰ ÝËM#ž9Yµ6U6UÛ¥ÆF†õ¥ Rñ\;Ä-uM݉Ԇ"•ê&©KØúU¨PÓÊêj8 Hñm¾´@h6¬ÙV ¦I#z Ö¨HÅRõ bÕœ8hŸá™õ¾DûÜ©Rš¶hÝû*¶À4 ! ©ºÓ6ê ÅØM³hÀ–Pq­Hsóô²Ó.³±r6þ<Ÿñ\X¡¨{î!÷çäc•;•Jj0;}šÖÎàž9é”ÜàÚÈó;ÛawÐà3‡•&›¦Åh£[Š?X„;JÝÎRµmè¨'ZèÒ“‚vÌUÝ1rµæ0ÍOó™ûÚêî(V.ShÒ‰Y–lt­ ”²à¬©Î6½ ×n¤bY îóØÐ#±‰€Ôk²†¡Òxâ¦Ö¨piöѦB¨bSZ$eSN<ÌÞž@ÃíNSe«%Ä·ùPšAÊ=ÛŽÐÒ¸.a‘´Pl¨(á1°ÓM5•¢eBÖæ·G˜‚sœR¾*BÏTöQº¹7º˜{k…¢­¼4eºÉâ}õ$…deÎè}…g&v÷:‰u0V,H0nýe>+,töj‰r§•÷Ç e²9ÄA,é’)sTÌÆ…n°jk½ç{¬Lo™µ~5;©mX—€I emÇ z:2M(Ëy©²°I5ų³À3¥[tìØ·ù¼…«;K[N–ÜO>áº(ZÅF·¡@SW§@{±gCz¦6޼4%^EÙMµšs.†vZŒölpºDbÒ&¬·ÿã×ïaˆrl9Ó°am)G,¦4¾Ä…F Àî¿Ìç´ƒŠ7œ¹æŽ¹¯† BÏ„(iVO†!9ñîll¸áÚªl—;º†ì˜µÎš­Rñò¨-¸δzÚðàßýS#íRwR¯%’Ô£¹™RKivX°D”C¡ÌÛ|¹“¾üϤpü}RÀ”y^lŠ÷à®Ô†—¦ÑR:”©I0kN“Nj¨;Õh½95EÜ2”ŠÚ¬¡1H|A`½sfïZ•A-lR ´¶¦SêÅvpÓm8—»òûfYR±ñ§ùÌWÍC{õž~JQ!ÞÕY²ÖPvY-ØmbV1†O) FóX/ ®1 ­bé!ÂànXWB³M Ȓæ§ùkX[ëÙYöx4R”Æõž4 §k–ž¼ŸœqSú©ܵ›•#ˆ§™‚ ž4§uÖ6R%í3w¤°”—LÖâN{O7g,+¶TZ]R ²õà*[,Âýè ËɱD,»·r6žæçù±; …ÜÁ4Ä[nmN¬M߃Í@`ÌÕ$ju丽“,§NqœeE؈¿FAoEiÔ¶¹Ó(Z»›ºQ´/´XÒ6Ü©{ÆŽ´Ò ©e§yO ×`¯ö—ùÁºS¢#šÛ¥Ã!PC/wLñ`Ëb ë'Y®†z2ÖCjܧÊ)*;Ý¢bH‘Þ©²%¦¼”pˆds_tkS£D1-®œ* Tï %j‰4½ÚXømJc)Gó6?XØ‹HcÎ&ϱ©µá©ÐØ´¹7³Óäd§ïtT¦M…¦i3ë1ÏiVlÎx±¬!®PE zg‚{Uš¢SκÁ]¡ái8t*,Jµ(„zQKxÞæ³ÀâÆVØ´;Ç^nk ]HmèEÙ0!@\ ÎJ‘êB=ÉjÚ9†÷K-/ŠÌûÄÀ-³nQظf/ÚДDKîD-®híå¼(CS\v¯ÆŠžÆ­ÛÞæÇVmwJíÆ l¡s̲ YµXíì(+Ù¬)Bž³ ²¤WÁ® ·ŸjM7„—`é•õ<ŽXµ”¦AŠwêW¥iÎUquuTÄÔ&ÙBÖÞ4'¶%‚öm~\ R™%¢9Øž ´éæì ëæ©`A¼¦ MuÊ4E[ ¢ 6ÙìP·³çQ ô 2ÎýhÜZ¤ÛM9¤’ÂGßó_Ý|cùÖò­åOaÂGá#ùèÁ¿ñø•o•o•o,#„ÂGá£ïù‡ß.^n¾±|kùÖòç0‚|$ÉGþMÂ˯|«|cùÖò'1> …¾ç~àW¾µ|«|cù³øßhC9¬˜¼IEND®B`‚ubuntu-push-0.68+16.04.20160310.2/docs/example-client/helloHelper-apparmor.json0000644000015600001650000000020512670364255027266 0ustar pbuserpbgroup00000000000000{ "template": "ubuntu-push-helper", "policy_groups": [ "push-notification-client" ], "policy_version": 1.2 } ubuntu-push-0.68+16.04.20160310.2/docs/example-client/push-example.qmlproject0000644000015600001650000000167612670364255027040 0ustar pbuserpbgroup00000000000000import QmlProject 1.1 Project { mainFile: "main.qml" /* Include .qml, .js, and image files from current directory and subdirectories */ QmlFiles { directory: "." } JavaScriptFiles { directory: "." } ImageFiles { directory: "." } Files { filter: "*.desktop" } Files { filter: "www/*.html" } Files { filter: "Makefile" } Files { directory: "www" filter: "*" } Files { directory: "www/img/" filter: "*" } Files { directory: "www/css/" filter: "*" } Files { directory: "www/js/" filter: "*" } Files { directory: "tests/" filter: "*" } Files { directory: "debian" filter: "*" } /* List of plugin directories passed to QML runtime */ importPaths: [ "." ,"/usr/bin","/usr/lib/x86_64-linux-gnu/qt5/qml" ] } ubuntu-push-0.68+16.04.20160310.2/docs/example-client/tests/0000755000015600001650000000000012670364532023454 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/docs/example-client/tests/autopilot/0000755000015600001650000000000012670364532025474 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/docs/example-client/tests/autopilot/run0000644000015600001650000000026312670364255026226 0ustar pbuserpbgroup00000000000000#!/bin/bash if [[ -z `which autopilot` ]]; then echo "Autopilot is not installed. Skip" exit fi SCRIPTPATH=`dirname $0` pushd ${SCRIPTPATH} autopilot run push-example popd ubuntu-push-0.68+16.04.20160310.2/docs/example-client/tests/autopilot/push-example/0000755000015600001650000000000012670364532030104 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/docs/example-client/tests/autopilot/push-example/__init__.py0000644000015600001650000000460012670364255032217 0ustar pbuserpbgroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- """Ubuntu Touch App autopilot tests.""" import os import subprocess from autopilot import input, platform from autopilot.matchers import Eventually from testtools.matchers import Equals from ubuntuuitoolkit import base, emulators def _get_module_include_path(): return os.path.join(get_path_to_source_root(), 'modules') def get_path_to_source_root(): return os.path.abspath( os.path.join( os.path.dirname(__file__), '..', '..', '..', '..')) class ClickAppTestCase(base.UbuntuUIToolkitAppTestCase): """Common test case that provides several useful methods for the tests.""" package_id = '' # TODO app_name = 'push-example' def setUp(self): super(ClickAppTestCase, self).setUp() self.pointing_device = input.Pointer(self.input_device_class.create()) self.launch_application() self.assertThat(self.main_view.visible, Eventually(Equals(True))) def launch_application(self): if platform.model() == 'Desktop': self._launch_application_from_desktop() else: self._launch_application_from_phablet() def _launch_application_from_desktop(self): app_qml_source_location = self._get_app_qml_source_path() if os.path.exists(app_qml_source_location): self.app = self.launch_test_application( base.get_qmlscene_launch_command(), '-I' + _get_module_include_path(), app_qml_source_location, app_type='qt', emulator_base=emulators.UbuntuUIToolkitEmulatorBase) else: raise NotImplementedError( "On desktop we can't install click packages yet, so we can " "only run from source.") def _get_app_qml_source_path(self): qml_file_name = '{0}.qml'.format(self.app_name) return os.path.join(self._get_path_to_app_source(), qml_file_name) def _get_path_to_app_source(self): return os.path.join(get_path_to_source_root(), self.app_name) def _launch_application_from_phablet(self): # On phablet, we only run the tests against the installed click # package. self.app = self.launch_click_package(self.pacakge_id, self.app_name) @property def main_view(self): return self.app.select_single(emulators.MainView) ubuntu-push-0.68+16.04.20160310.2/docs/example-client/tests/autopilot/push-example/test_main.py0000644000015600001650000000141612670364255032445 0ustar pbuserpbgroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- """Tests for the Hello World""" import os from autopilot.matchers import Eventually from testtools.matchers import Equals import push-example class MainViewTestCase(push-example.ClickAppTestCase): """Generic tests for the Hello World""" def test_initial_label(self): label = self.main_view.select_single(objectName='label') self.assertThat(label.text, Equals('Hello..')) def test_click_button_should_update_label(self): button = self.main_view.select_single(objectName='button') self.pointing_device.click_object(button) label = self.main_view.select_single(objectName='label') self.assertThat(label.text, Eventually(Equals('..world!'))) ubuntu-push-0.68+16.04.20160310.2/docs/example-client/tests/unit/0000755000015600001650000000000012670364532024433 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/docs/example-client/tests/unit/tst_hellocomponent.qml0000644000015600001650000000223212670364255031067 0ustar pbuserpbgroup00000000000000import QtQuick 2.0 import QtTest 1.0 import Ubuntu.Components 0.1 import "../../components" // See more details @ http://qt-project.org/doc/qt-5.0/qtquick/qml-testcase.html // Execute tests with: // qmltestrunner Item { // The objects HelloComponent { id: objectUnderTest } TestCase { name: "HelloComponent" function init() { console.debug(">> init"); compare("",objectUnderTest.text,"text was not empty on init"); console.debug("<< init"); } function cleanup() { console.debug(">> cleanup"); console.debug("<< cleanup"); } function initTestCase() { console.debug(">> initTestCase"); console.debug("<< initTestCase"); } function cleanupTestCase() { console.debug(">> cleanupTestCase"); console.debug("<< cleanupTestCase"); } function test_canReadAndWriteText() { var expected = "Hello World"; objectUnderTest.text = expected; compare(expected,objectUnderTest.text,"expected did not equal result"); } } } ubuntu-push-0.68+16.04.20160310.2/docs/example-client/README0000644000015600001650000000155212670364255023177 0ustar pbuserpbgroup00000000000000Example App for the QML notifications API. This is an example application showing how to use push notifications in Ubuntu Touch devices. = Running on Ubuntu Touch = Since push is currently only meant for Ubuntu Touch devices, this is meant to be used in the emulator or on a real device. * Open the example project in Ubuntu-SDK * Build a click file * Run in the emulator or device = Running on the desktop = This is more complicated but may be convenient while experimenting: * Install qtdeclarative5-ubuntu-push-notifications-plugin * Install ubuntu-push-client * Run ubuntu-push-client in trivial helper mode: UBUNTU_PUSH_USE_TRIVIAL_HELPER=1 ./ubuntu-push-client * Build click package * Install in your desktop: sudo click install --all-users com.ubuntu.developer.push.ubuntu-push-example_0.1_all.click * Run example app from the SDK using the "Desktop" kit ubuntu-push-0.68+16.04.20160310.2/docs/example-client/hello.desktop0000644000015600001650000000020012670364255025002 0ustar pbuserpbgroup00000000000000[Desktop Entry] Name=hello Exec=qmlscene $@ main.qml Icon=push-example.png Terminal=false Type=Application X-Ubuntu-Touch=true ubuntu-push-0.68+16.04.20160310.2/docs/example-client/helloHelper0000755000015600001650000000013512670364255024504 0ustar pbuserpbgroup00000000000000#!/usr/bin/python3 import sys f1, f2 = sys.argv[1:3] open(f2, "w").write(open(f1).read()) ubuntu-push-0.68+16.04.20160310.2/docs/example-client/components/0000755000015600001650000000000012670364532024477 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/docs/example-client/components/ChatClient.qml0000644000015600001650000000623712670364255027242 0ustar pbuserpbgroup00000000000000import QtQuick 2.0 import Ubuntu.Components 0.1 Item { property string nick property string token property bool registered: false signal error (string msg) onNickChanged: { if (nick) { register() } else { registered = false } } onTokenChanged: {register()} function register() { console.log("registering ", nick, token); if (nick && token) { var req = new XMLHttpRequest(); req.open("post", "http://direct.ralsina.me:8001/register", true); req.setRequestHeader("Content-type", "application/json"); req.onreadystatechange = function() {//Call a function when the state changes. if(req.readyState == 4) { if (req.status == 200) { registered = true; } else { error(JSON.parse(req.responseText)["error"]); } } } req.send(JSON.stringify({ "nick" : nick.toLowerCase(), "token": token })) } } /* options is of the form: { enabled: false, persist: false, popup: false, sound: "buzz.mp3", vibrate: false, counter: 5 } */ function sendMessage(message, options) { var to_nick = message["to"] var data = { "from_nick": nick.toLowerCase(), "from_token": token, "nick": to_nick.toLowerCase(), "data": { "message": message, "notification": {} } } if (options["enabled"]) { data["data"]["notification"] = { "card": { "summary": nick + " says:", "body": message["message"], "popup": options["popup"], "persist": options["persist"], "actions": ["appid://com.ubuntu.developer.ralsina.hello/hello/current-user-version"] } } if (options["sound"]) { data["data"]["notification"]["sound"] = options["sound"] } if (options["vibrate"]) { data["data"]["notification"]["vibrate"] = { "duration": 200 } } if (options["counter"]) { data["data"]["notification"]["emblem-counter"] = { "count": Math.floor(options["counter"]), "visible": true } } } var req = new XMLHttpRequest(); req.open("post", "http://direct.ralsina.me:8001/message", true); req.setRequestHeader("Content-type", "application/json"); req.onreadystatechange = function() {//Call a function when the state changes. if(req.readyState == 4) { if (req.status == 200) { registered = true; } else { error(JSON.parse(req.responseText)["error"]); } } } req.send(JSON.stringify(data)) } } ubuntu-push-0.68+16.04.20160310.2/docs/example-client/.excludes0000644000015600001650000000000012670364255024117 0ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/docs/example-client/helloHelper.json0000644000015600001650000000003612670364255025451 0ustar pbuserpbgroup00000000000000{ "exec": "helloHelper" } ubuntu-push-0.68+16.04.20160310.2/docs/example-client/main.qml0000644000015600001650000002407012670364255023756 0ustar pbuserpbgroup00000000000000import QtQuick 2.0 import Qt.labs.settings 1.0 import Ubuntu.Components 0.1 import Ubuntu.Components.ListItems 0.1 as ListItem import Ubuntu.PushNotifications 0.1 import "components" MainView { id: "mainView" // objectName for functional testing purposes (autopilot-qt5) objectName: "mainView" // Note! applicationName needs to match the "name" field of the click manifest applicationName: "com.ubuntu.developer.ralsina.hello" automaticOrientation: true useDeprecatedToolbar: false width: units.gu(100) height: units.gu(75) Settings { property alias nick: chatClient.nick property alias nickText: nickEdit.text property alias nickPlaceholder: nickEdit.placeholderText property alias nickEnabled: nickEdit.enabled } states: [ State { name: "no-push-token" when: (pushClient.token == "") PropertyChanges { target: nickEdit; readOnly: true} PropertyChanges { target: nickEdit; focus: true} PropertyChanges { target: messageEdit; enabled: false} PropertyChanges { target: loginButton; enabled: false} PropertyChanges { target: loginButton; text: "Login"} }, State { name: "push-token-not-registered" when: ((pushClient.token != "") && (chatClient.registered == false)) PropertyChanges { target: nickEdit; readOnly: false} PropertyChanges { target: nickEdit; text: ""} PropertyChanges { target: nickEdit; focus: true} PropertyChanges { target: messageEdit; enabled: false} PropertyChanges { target: loginButton; enabled: true} PropertyChanges { target: loginButton; text: "Login"} }, State { name: "registered" when: ((pushClient.token != "") && (chatClient.registered == true)) PropertyChanges { target: nickEdit; readOnly: true} PropertyChanges { target: nickEdit; text: "Your nick is " + chatClient.nick} PropertyChanges { target: messageEdit; focus: true} PropertyChanges { target: messageEdit; enabled: true} PropertyChanges { target: loginButton; enabled: true} PropertyChanges { target: loginButton; text: "Logout"} } ] state: "no-push-token" ChatClient { id: chatClient onError: {messageList.handle_error(msg)} token: pushClient.token } PushClient { id: pushClient Component.onCompleted: { notificationsChanged.connect(messageList.handle_notifications) error.connect(messageList.handle_error) onTokenChanged: { console.log("foooooo") } } appId: "com.ubuntu.developer.ralsina.hello_hello" } TextField { id: nickEdit placeholderText: "Your nickname" inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText | Qt.ImhPreferLowercase anchors.left: parent.left anchors.right: loginButton.left anchors.top: parent.top anchors.leftMargin: units.gu(.5) anchors.rightMargin: units.gu(1) anchors.topMargin: units.gu(.5) onAccepted: { loginButton.clicked() } } Button { id: loginButton anchors.top: nickEdit.top anchors.right: parent.right anchors.rightMargin: units.gu(.5) onClicked: { if (chatClient.nick) { // logout chatClient.nick = "" } else { // login chatClient.nick = nickEdit.text } } } TextField { id: messageEdit inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText | Qt.ImhPreferLowercase anchors.right: parent.right anchors.left: parent.left anchors.top: nickEdit.bottom anchors.topMargin: units.gu(1) anchors.rightMargin: units.gu(.5) anchors.leftMargin: units.gu(.5) placeholderText: "Your message" onAccepted: { console.log("sending " + text) var idx = text.indexOf(":") var nick_to = text.substring(0, idx).trim() var msg = text.substring(idx+1, 9999).trim() var i = { "from" : chatClient.nick, "to" : nick_to, "message" : msg } var o = { enabled: annoyingSwitch.checked, persist: persistSwitch.checked, popup: popupSwitch.checked, sound: soundSwitch.checked, vibrate: vibrateSwitch.checked, counter: counterSlider.value } chatClient.sendMessage(i, o) i["type"] = "sent" messagesModel.insert(0, i) text = "" } } ListModel { id: messagesModel ListElement { from: "" to: "" type: "info" message: "Register by typing your nick and clicking Login." } ListElement { from: "" to: "" type: "info" message: "Send messages in the form \"destination: hello\"" } ListElement { from: "" to: "" type: "info" message: "Slide from the bottom to control notification behaviour." } } UbuntuShape { anchors.left: parent.left anchors.right: parent.right anchors.bottom: notificationSettings.bottom anchors.top: messageEdit.bottom anchors.topMargin: units.gu(1) ListView { id: messageList model: messagesModel anchors.fill: parent delegate: Rectangle { MouseArea { anchors.fill: parent onClicked: { if (from != "") { messageEdit.text = from + ": " messageEdit.focus = true } } } height: label.height + units.gu(2) width: parent.width Rectangle { color: { "info": "#B5EBB9", "received" : "#A2CFA5", "sent" : "#FFF9C8", "error" : "#FF4867"}[type] height: label.height + units.gu(1) anchors.fill: parent radius: 5 anchors.margins: units.gu(.5) Text { id: label text: "" + ((type=="sent")?to:from) + ": " + message wrapMode: Text.Wrap width: parent.width - units.gu(1) x: units.gu(.5) y: units.gu(.5) horizontalAlignment: (type=="sent")?Text.AlignRight:Text.AlignLeft } } } function handle_error(error) { messagesModel.insert(0, { "from" : "", "to" : "", "type" : "error", "message" : "ERROR: " + error + "" }) } function handle_notifications(list) { list.forEach(function(notification) { var item = JSON.parse(notification) item["type"] = "received" messagesModel.insert(0, item) }) } } } Panel { id: notificationSettings anchors { left: parent.left right: parent.right bottom: parent.bottom } height: item1.height * 9 UbuntuShape { anchors.fill: parent color: Theme.palette.normal.overlay Column { id: settingsColumn anchors.fill: parent ListItem.Header { text: "Notification Settings" } ListItem.Standard { id: item1 text: "Enable Notifications" control: Switch { id: annoyingSwitch checked: true } } ListItem.Standard { text: "Enable Popup" enabled: annoyingSwitch.checked control: Switch { id: popupSwitch checked: true } } ListItem.Standard { text: "Persistent" enabled: annoyingSwitch.checked control: Switch { id: persistSwitch checked: true } } ListItem.Standard { text: "Make Sound" enabled: annoyingSwitch.checked control: Switch { id: soundSwitch checked: true } } ListItem.Standard { text: "Vibrate" enabled: annoyingSwitch.checked control: Switch { id: vibrateSwitch checked: true } } ListItem.Standard { text: "Counter Value" enabled: annoyingSwitch.checked control: Slider { id: counterSlider value: 42 } } Button { text: "Set Counter Via Plugin" onClicked: { pushClient.count = counterSlider.value; } } Button { text: "Clear Persistent Notifications" onClicked: { pushClient.clearPersistent([]); } } } } } } ubuntu-push-0.68+16.04.20160310.2/docs/example-client/manifest.json0000644000015600001650000000105612670364255025017 0ustar pbuserpbgroup00000000000000{ "architecture": "all", "description": "Example app for Ubuntu push notifications.", "framework": "ubuntu-sdk-14.10", "hooks": { "hello": { "apparmor": "hello.json", "desktop": "hello.desktop" }, "helloHelper": { "apparmor": "helloHelper-apparmor.json", "push-helper": "helloHelper.json" } }, "maintainer": "Roberto Alsina ", "name": "com.ubuntu.developer.ralsina.hello", "title": "Hello", "version": "0.4.4" } ubuntu-push-0.68+16.04.20160310.2/docs/example-client/Makefile0000644000015600001650000000101312670364255023747 0ustar pbuserpbgroup00000000000000# More information: https://wiki.ubuntu.com/Touch/Testing # # Notes for autopilot tests: # ----------------------------------------------------------- # In order to run autopilot tests: # sudo apt-add-repository ppa:autopilot/ppa # sudo apt-get update # sudo apt-get install python-autopilot autopilot-qt ############################################################# all: autopilot: chmod +x tests/autopilot/run tests/autopilot/run check: qmltestrunner -input tests/unit run: /usr/bin/qmlscene $@ push-example.qml ubuntu-push-0.68+16.04.20160310.2/docs/example-client/hello.json0000644000015600001650000000016412670364255024313 0ustar pbuserpbgroup00000000000000{ "policy_groups": [ "networking", "push-notification-client" ], "policy_version": 1.2 }ubuntu-push-0.68+16.04.20160310.2/docs/example-server/0000755000015600001650000000000012670364532022342 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/docs/example-server/.gitignore0000644000015600001650000000003512670364255024332 0ustar pbuserpbgroup00000000000000.bzr .bzrignore node_modules ubuntu-push-0.68+16.04.20160310.2/docs/example-server/app.js0000644000015600001650000001670412670364255023472 0ustar pbuserpbgroup00000000000000var express = require('express') , bodyParser = require('body-parser') var Registry = require('./lib/registry') , Inbox = require('./lib/inbox') , Notifier = require('./lib/notifier') function wire(db, cfg) { var reg, inbox, notifier, app // control whether not to have persistent inboxes var no_inbox = cfg.no_inbox app = express() reg = new Registry(db) if (!no_inbox) { inbox = new Inbox(db) } else { inbox = null } var push_url = process.env.PUSH_URL || cfg.push_url notifier = new Notifier(push_url, cfg) notifier.on('unknownToken', function(nick, token) { reg.removeToken(nick, token, function() {}, function(err) { app.emit('mongoError', err) }) }) notifier.on('pushError', function(err, resp, body) { app.emit('pushError', err, resp, body) }) function unavailable(resp, err) { app.emit('mongoError', err) var ctype = resp.get('Content-Type') if (ctype&&ctype.substr(0,10) == 'text/plain') { resp.send(503, "db is hopefully only momentarily :(\n") } else { resp.json(503, {error: "unavailable"}) } } app.get("/_check", function(req, resp) { db.command({ping: 1}, function(err, res) { if(!err) { resp.json({ok: true}) } else { unavailable(resp, err) } }) }) app.use(bodyParser.json()) app.use('/play-notify-form', bodyParser.urlencoded({extended: false})) app.use(function(err, req, resp, next) { resp.json(err.status, {error: "invalid", message: err.message}) }) app.get("/", function(req, resp) { if (!cfg.play_notify_form) { resp.sendfile(__dirname + '/index.html') } else { resp.sendfile(__dirname + '/notify-form.html') } }) // NB: simplified, minimal identity and auth piggybacking on push tokens /* POST /register let's register a pair nick, token taking a JSON obj: { "nick": string, "token": token-string } */ app.post("/register", function(req, resp) { if(typeof(req.body.token) != "string" || typeof(req.body.nick) != "string" || req.body.token == "" || req.body.nick == "") { resp.json(400, {"error": "invalid"}) return } reg.insertToken(req.body.nick, req.body.token, function() { resp.json({ok: true}) }, function() { resp.json(400, {"error": "dup"}) }, function(err) { unavailable(resp, err) }) }) function checkToken(nick, token, okCb, resp) { function bad() { resp.json(401, {error: "unauthorized"}) } reg.findToken(nick, function(nickToken) { if (nickToken == token) { okCb() return } bad() }, bad, function(err) { unavailable(resp, err) }) } /* doNotify ephemeral is true: message not put in the inbox, _ephemeral flag added ephemeral is false: message put in inbox, with added unix _timestamp and increasing _serial */ function doNotify(ephemeral, nick, data, okCb, unknownNickCb, resp) { function notify(token, data) { notifier.notify(nick, token, data) okCb() } reg.findToken(nick, function(token) { if (ephemeral||no_inbox) { data._ephemeral = Date.now() notify(token, data) } else { inbox.pushMessage(token, data, function(msg) { notify(token, msg) }, function(err) { unavailable(resp, err) }) } }, function() { // not found unknownNickCb() }, function(err) { unavailable(resp, err) }) } /* POST /message let's send a message to nick taking a JSON obj: { "nick": string, "data": data, "from_nick": string, "from_token": string} */ app.post("/message", function(req, resp) { if (!req.body.data||!req.body.nick||!req.body.from_token||!req.body.from_nick) { resp.json(400, {"error": "invalid"}) return } checkToken(req.body.from_nick, req.body.from_token, function() { var data = req.body.data data._from = req.body.from_nick doNotify(false, req.body.nick, data, function() { resp.json({ok: true}) }, function() { // not found resp.json(400, {"error": "unknown-nick"}) }, resp) }, resp) }) if (!no_inbox) { // /drain supported only if no_inbox false /* POST /drain let's get pending messages for nick+token: it removes messages older than timestamp and return newer ones { "nick": string, "token": string, "timestamp": unix-timestamp } */ app.post("/drain", function(req, resp) { if(!req.body.token||!req.body.nick|| typeof(req.body.timestamp) != "number") { resp.json(400, {"error": "invalid"}) return } checkToken(req.body.nick, req.body.token, function() { inbox.drain(req.body.token, req.body.timestamp, function(msgs) { resp.json(200, {ok: true, msgs: msgs}) }, function(err) { unavailable(resp, err) }) }, resp) }) } /* Form /play-notify-form messages sent through the form are ephemeral, just transmitted through PN, without being pushed into the inbox */ if (cfg.play_notify_form) { app.post("/play-notify-form", function(req, resp) { if (!req.body.message||!req.body.nick) { resp.redirect("/?error=invalid or empty fields in form") return } var data = { "message": { "from": "website", "message": req.body.message, "to": req.body.nick.toLowerCase() }, "notification": { } } if (req.body.enable) { var card = { "summary": "The website says:", "body": req.body.message, "actions": ["appid://com.ubuntu.developer.ralsina.hello/hello/current-user-version"] } if (req.body.popup) {card["popup"] = true} if (req.body.persist) {card["persist"] = true} data["notification"]["card"] = card if (req.body.sound) {data["notification"]["sound"] = true} if (req.body.vibrate) {data["notification"]["vibrate"] = {"duration": 200}} if (req.body.counter) {data["notification"]["emblem-counter"] = { "count": Math.floor(req.body.counter), "visible": true }} } doNotify(true, req.body.nick, data, function() { resp.redirect("/") }, function() { // not found resp.redirect("/?error=unknown nick") }, resp) }) } // for testing app.set('_reg', reg) app.set('_inbox', inbox) app.set('_notifier', notifier) return app } exports.wire = wire ubuntu-push-0.68+16.04.20160310.2/docs/example-server/package.json0000644000015600001650000000103512670364255024631 0ustar pbuserpbgroup00000000000000{ "name": "pushAppServer", "version": "0.0.2", "description": "Push Notifications App Server Example", "main": "server.js", "dependencies": { "body-parser": "~1.4.3", "express": "~4.4.5", "mongodb": "~1.4.7", "request": "~2.36.0" }, "scripts": { "test": "mocha --ui tdd", "start": "PUSH_URL=http://localhost:8080 node server.js" }, "author": "Canonical", "license": "LGPL", "directories": { "test": "test" }, "devDependencies": { "mocha": "~1.20.1", "supertest": "~0.13.0" } } ubuntu-push-0.68+16.04.20160310.2/docs/example-server/test/0000755000015600001650000000000012670364532023321 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/docs/example-server/test/inbox_test.js0000644000015600001650000000466512670364255026052 0ustar pbuserpbgroup00000000000000var assert = require('assert') var MongoClient = require('mongodb').MongoClient function unexpected(msg) { assert.ok(false, "unexpected: "+msg) } var Inbox = require('../lib/inbox') suite('Inbox', function(){ setup(function(done) { var self = this MongoClient.connect("mongodb://localhost:27017/pushAppTestDb", function(err, database) { if(err) throw err self.db = database // cleanup self.db.collection('inbox').drop(function(err) { if(err && err.errmsg != 'ns not found') throw err done() }) }) }) test('push-1', function(done) { var inbox = new Inbox(this.db) inbox.pushMessage('foo', {m: 42}, function(msg) { assert.ok(msg._timestamp) assert.equal(msg._serial, 1) done() }, function(err) { unexpected(err) }) }) test('push-2', function(done) { var inbox = new Inbox(this.db) inbox.pushMessage('foo', {m: 42}, function(msg) { assert.equal(msg._serial, 1) inbox.pushMessage('foo', {m: 45}, function(msg) { assert.equal(msg._serial, 2) done() }, function(err) { unexpected(err) }) }, function(err) { unexpected(err) }) }) test('push-3-drain', function(done) { var inbox = new Inbox(this.db) inbox.pushMessage('foo', {m: 42, _timestamp: 2000}, function(msg) { inbox.pushMessage('foo', {m: 45, _timestamp: 3000}, function(msg) { inbox.pushMessage('foo', {m: 47, _timestamp: 4000}, function(msg) { inbox.drain('foo', 3000, function(msgs) { assert.deepEqual(msgs, [ {m: 45, _timestamp: 3000, serial: 2}, {m: 47, _timestamp: 4000, serial: 3} ]) inbox.drain('foo', 0, function(msgs) { assert.equal(msgs.length, 2) done() }, done) }, done) }, done) }, done) }, done) }) test('drain-nop', function(done) { var inbox = new Inbox(this.db) inbox.drain('foo', 3000, function(msgs) { assert.deepEqual(msgs, []) done() }, done) }) })ubuntu-push-0.68+16.04.20160310.2/docs/example-server/test/notifier_test.js0000644000015600001650000001276512670364255026552 0ustar pbuserpbgroup00000000000000var assert = require('assert') , http = require('http') var Notifier = require('../lib/notifier') var cfg = { 'app_id': 'app1', 'expire_mins': 10, 'retry_secs': 0.05, 'retry_batch': 1, 'happy_retry_secs': 0.02 } , cfg_batch2 = { 'app_id': 'app1', 'expire_mins': 10, 'retry_secs': 0.05, 'retry_batch': 2, 'happy_retry_secs': 0.02 } suite('Notifier', function() { setup(function(done) { var self = this self.s = http.createServer(function(req, resp) { self.s.emit(req.method, req, resp) }) self.s.listen(0, 'localhost', function() { self.url = 'http://localhost:' + self.s.address().port done() }) }) teardown(function() { this.s.close() }) test('happy-notify', function(done) { var b = "" this.s.on('POST', function(req, resp) { req.on('data', function(chunk) { b += chunk }) req.on('end', function() { resp.writeHead(200, {"Content-Type": "application/json"}) resp.end('{}') }) }) var n = new Notifier(this.url, cfg) var approxExpire = new Date approxExpire.setUTCMinutes(approxExpire.getUTCMinutes()+10) n.notify("N", "T", {m: 42}, function() { var reqObj = JSON.parse(b) var expireOn = Date.parse(reqObj.expire_on) delete reqObj.expire_on assert.ok(expireOn >= approxExpire) assert.deepEqual(reqObj, { "token": "T", "appid": "app1", "data": {"m": 42} }) done() }) }) test('retry-notify', function(done) { var b = "" var fail = 1 this.s.on('POST', function(req, resp) { if (fail) { fail-- resp.writeHead(503, {"Content-Type": "application/json"}) resp.end('') return } req.on('data', function(chunk) { b += chunk }) req.on('end', function() { resp.writeHead(200, {"Content-Type": "application/json"}) resp.end('{}') }) }) var n = new Notifier(this.url, cfg) var approxExpire = new Date approxExpire.setUTCMinutes(approxExpire.getUTCMinutes()+10) n.notify("N", "T", {m: 42}, function() { var reqObj = JSON.parse(b) var expireOn = Date.parse(reqObj.expire_on) delete reqObj.expire_on assert.ok(expireOn >= approxExpire) assert.deepEqual(reqObj, { "token": "T", "appid": "app1", "data": {"m": 42} }) done() }) }) function flakyPOST(s, fail, tokens) { s.on('POST', function(req, resp) { var b = "" req.on('data', function(chunk) { b += chunk }) req.on('end', function() { var reqObj = JSON.parse(b) if (fail) { fail-- resp.writeHead(503, {"Content-Type": "application/json"}) resp.end('') return } tokens[reqObj.token] = 1 resp.writeHead(200, {"Content-Type": "application/json"}) resp.end('{}') }) }) } test('retry-notify-2-retries', function(done) { var tokens = {} flakyPOST(this.s, 2, tokens) var n = new Notifier(this.url, cfg) function yay() { assert.deepEqual(tokens, {"T1": 1}) done() } n.notify("N1", "T1", {m: 42}, yay) }) test('retry-notify-2-batches', function(done) { var tokens = {} flakyPOST(this.s, 2, tokens) var n = new Notifier(this.url, cfg) var waiting = 2 function yay() { waiting-- if (waiting == 0) { assert.deepEqual(tokens, {"T1": 1, "T2": 1}) done() } } n.notify("N1", "T1", {m: 42}, yay) n.notify("N2", "T2", {m: 42}, yay) }) test('retry-notify-expired', function(done) { var tokens = {} flakyPOST(this.s, 2, tokens) var n = new Notifier(this.url, cfg_batch2) var waiting = 1 function yay() { waiting-- if (waiting == 0) { assert.deepEqual(tokens, {"T2": 1}) done() } } n.notify("N1", "T1", {m: 42}, yay, new Date) n.notify("N2", "T2", {m: 42}, yay) }) test('unknown-token-notify', function(done) { this.s.on('POST', function(req, resp) { resp.writeHead(400, {"Content-Type": "application/json"}) resp.end('{"error": "unknown-token"}') }) var n = new Notifier(this.url, cfg) n.on('unknownToken', function(nick, token) { assert.equal(nick, "N") assert.equal(token, "T") done() }) n.notify("N", "T", {m: 42}) }) test('error-notify', function(done) { this.s.on('POST', function(req, resp) { resp.writeHead(500) resp.end('') }) var n = new Notifier(this.url, cfg) n.on('pushError', function(err, resp, body) { assert.equal(resp.statusCode, 500) done() }) n.notify("N", "T", {m: 42}) }) }) ubuntu-push-0.68+16.04.20160310.2/docs/example-server/test/app_test.js0000644000015600001650000005273312670364255025512 0ustar pbuserpbgroup00000000000000var assert = require('assert') , http = require('http') , request = require('supertest') var app = require('../app') var cfg = { 'app_id': 'app1', 'push_url': 'http://push', 'expire_mins': 10, 'retry_secs': 0.05, 'retry_batch': 1, 'happy_retry_secs': 0.02, } function cloneCfg() { return JSON.parse(JSON.stringify(cfg)) } var PLAY_NOTIFY_FORM = '/play-notify-form' suite('app', function() { setup(function() { this.db = {} this.app = app.wire(this.db, cloneCfg()) this.reg = this.app.get('_reg') this.inbox = this.app.get('_inbox') this.notifier = this.app.get('_notifier') }) test('wire', function() { assert.ok(this.reg) assert.ok(this.notifier) assert.equal(this.notifier.baseURL, 'http://push') var emitted this.app.on('pushError', function(err, resp, body) { emitted = [err, resp, body] }) this.notifier.pushError('err', 'resp', 'body') assert.deepEqual(emitted, ["err", "resp", "body"]) }) test('wire-unknownToken', function() { var got this.reg.removeToken = function(nick, token, doneCb, errCb) { got = [nick, token] doneCb() } this.notifier.emit('unknownToken', "N", "T") assert.deepEqual(got, ["N", "T"]) }) test('wire-unknownToken-mongoError', function() { var emitted this.app.on('mongoError', function(err) { emitted = err }) this.reg.removeToken = function(nick, token, doneCb, errCb) { errCb({}) } this.notifier.emit('unknownToken', "N", "T") assert.ok(emitted) }) test('_check', function(done) { var pingCmd this.db.command = function(cmd, cb) { pingCmd = cmd cb(null) } request(this.app) .get('/_check') .expect('Content-Type', 'application/json; charset=utf-8') .expect({ok: true}) .expect(200, function(err) { assert.deepEqual(pingCmd, {ping: 1}) done(err) }) }) test('_check-unavailable', function(done) { var pingCmd this.db.command = function(cmd, cb) { pingCmd = cmd cb({}) } request(this.app) .get('/_check') .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: "unavailable"}) .expect(503, function(err) { done(err) }) }) test('any-broken', function(done) { request(this.app) .post('/register') .set('Content-Type', 'application/json') .send("") .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: 'invalid', message: 'invalid json, empty body'}) .expect(400, done) }) test('register', function(done) { var got this.reg.insertToken = function(nick, token, doneCb, dupCb, errCb) { got = [nick, token] doneCb() } request(this.app) .post('/register') .set('Content-Type', 'application/json') .send({nick: "N", token: "T"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({ok: true}) .expect(200, function(err) { assert.deepEqual(got, ["N", "T"]) done(err) }) }) test('register-invalid', function(done) { request(this.app) .post('/register') .set('Content-Type', 'application/json') .send({token: "T"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: 'invalid'}) .expect(400, done) }) test('register-dup', function(done) { this.reg.insertToken = function(nick, token, doneCb, dupCb, errCb) { dupCb() } request(this.app) .post('/register') .set('Content-Type', 'application/json') .send({nick: "N", token: "T"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: 'dup'}) .expect(400, done) }) test('register-unavailable', function(done) { this.reg.insertToken = function(nick, token, doneCb, dupCb, errCb) { errCb({}) } request(this.app) .post('/register') .set('Content-Type', 'application/json') .send({nick: "N", token: "T"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: 'unavailable'}) .expect(503, done) }) test('message', function(done) { var lookup = [] var pushed var notify this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { lookup.push(nick) if (nick == "N") { foundCb("T") } else { foundCb("T2") } } this.inbox.pushMessage = function(token, msg, doneCb, errCb) { pushed = [token] msg._serial = 10 doneCb(msg) } this.notifier.notify = function(nick, token, data) { notify = [nick, token, data] } request(this.app) .post('/message') .set('Content-Type', 'application/json') .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({ok: true}) .expect(200, function(err) { assert.deepEqual(lookup, ["N", "N2"]) assert.deepEqual(pushed, ["T2"]) assert.deepEqual(notify, ["N2", "T2", {m: 1,_from: "N",_serial: 10}]) done(err) }) }) test('message-unauthorized', function(done) { this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { if (nick == "N") { foundCb("T") } else { notFoundCb() } } request(this.app) .post('/message') .set('Content-Type', 'application/json') .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "Z"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: "unauthorized"}) .expect(401, done) }) test('message-unknown-nick', function(done) { this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { if (nick == "N") { foundCb("T") } else { notFoundCb() } } request(this.app) .post('/message') .set('Content-Type', 'application/json') .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: "unknown-nick"}) .expect(400, done) }) test('message-invalid', function(done) { request(this.app) .post('/message') .set('Content-Type', 'application/json') .send({nick: "N"}) // missing data .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: 'invalid'}) .expect(400, done) }) test('message-check-token-unavailable', function(done) { var emitted this.app.on('mongoError', function(err) { emitted = err }) this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { if (nick == "N") { errCb({}) } else { foundCb("T2") } } request(this.app) .post('/message') .set('Content-Type', 'application/json') .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: 'unavailable'}) .expect(503, function(err) { assert.ok(emitted) done(err) }) }) test('message-inbox-unavailable', function(done) { this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { if (nick == "N") { foundCb("T") } else { foundCb("T2") } } this.inbox.pushMessage = function(token, msg, doneCb, errCb) { errCb({}) } request(this.app) .post('/message') .set('Content-Type', 'application/json') .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: 'unavailable'}) .expect(503, done) }) test('message-notify-unavailable', function(done) { this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { if (nick == "N") { foundCb("T") } else { errCb({}) } } request(this.app) .post('/message') .set('Content-Type', 'application/json') .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: 'unavailable'}) .expect(503, function(err) { done(err) }) }) test('index', function(done) { request(this.app) .get('/') .expect(new RegExp('pushAppServer')) .expect('Content-Type', 'text/html; charset=UTF-8') .expect(200, done) }) test('play-notify-form-absent', function(done) { request(this.app) .post(PLAY_NOTIFY_FORM) .type('form') .send({nick: "N", data: '{"m": 1}'}) .expect(404, done) }) test('drain', function(done) { var lookup var got var msgs = [{'m': 42, _timestamp: 4000, _serial: 20}] this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { lookup = [nick] foundCb("T") } this.inbox.drain = function(token, timestamp, doneCb, errCb) { got = [token, timestamp] doneCb(msgs) } request(this.app) .post('/drain') .set('Content-Type', 'application/json') .send({nick: "N", token: "T", timestamp: 4000}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({ok: true, msgs: msgs}) .expect(200, function(err) { assert.deepEqual(lookup, ["N"]) assert.deepEqual(got, ["T", 4000]) done(err) }) }) test('drain-unavailable', function(done) { var msgs = [{'m': 42, _timestamp: 4000, _serial: 20}] this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { foundCb("T") } this.inbox.drain = function(token, timestamp, doneCb, errCb) { errCb() } request(this.app) .post('/drain') .set('Content-Type', 'application/json') .send({nick: "N", token: "T", timestamp: 4000}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: 'unavailable'}) .expect(503, function(err) { done(err) }) }) test('drain-invalid', function(done) { request(this.app) .post('/drain') .set('Content-Type', 'application/json') .send({nick: "N"}) // missing data .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: 'invalid'}) .expect(400, done) }) test('drain-invalid-timestamp', function(done) { request(this.app) .post('/drain') .set('Content-Type', 'application/json') .send({nick: "N", token: "T", timestamp: "foo"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({error: 'invalid'}) .expect(400, done) }) }) suite('app-with-play-notify', function() { setup(function() { this.db = {} var cfg = cloneCfg() cfg.play_notify_form = true this.app = app.wire(this.db, cfg) this.reg = this.app.get('_reg') this.notifier = this.app.get('_notifier') }) test('root-form', function(done) { request(this.app) .get('/') .expect(new RegExp('<form.*action="' + PLAY_NOTIFY_FORM + '"(.|\n)*<input class="form-control" placeholder="Message destination" id="nick" name="nick" type="text"')) .expect('Content-Type', 'text/html; charset=UTF-8') .expect(200, done) }) test('play-notify-form', function(done) { var notify , start = Date.now() this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { foundCb("T") } this.notifier.notify = function(nick, token, data) { notify = [nick, token, data] } request(this.app) .post(PLAY_NOTIFY_FORM) .type('form') .send({nick: "N", message: 'foo'}) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Moved Temporarily. Redirecting to /') .expect(302, function(err) { assert.deepEqual(notify.slice(0, 2), ["N", "T"]) var data = notify[2] assert.equal(typeof(data._ephemeral), "number") assert.ok(data._ephemeral >= start) delete data._ephemeral assert.deepEqual(data, {"message":{"from":"website","message":"foo","to":"n"},"notification":{}}) done(err) }) }) test('play-notify-form-unknown-nick', function(done) { this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { notFoundCb() } request(this.app) .post(PLAY_NOTIFY_FORM) .set('Content-Type', 'application/json') .send({nick: "N", message: 'foo'}) .expect('Content-Type', 'text/plain; charset=utf-8') .expect("Moved Temporarily. Redirecting to /?error=unknown%20nick") .expect(302, done) }) test('play-notify-form-unavailable', function(done) { this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { errCb({}) } request(this.app) .post(PLAY_NOTIFY_FORM) .type('form') .send({nick: "N", message: 'foo'}) .expect('{"error":"unavailable"}') .expect(503, function(err) { done(err) }) }) test('play-notify-form-invalid', function(done) { this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { notFoundCb() } request(this.app) .post(PLAY_NOTIFY_FORM) .set('Content-Type', 'application/json') .send({nick: "", message: 'foo'}) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Moved Temporarily. Redirecting to /?error=invalid%20or%20empty%20fields%20in%20form') .expect(302, done) }) test('play-notify-form-broken', function(done) { request(this.app) .post(PLAY_NOTIFY_FORM) .type('form') .send("=") .expect('Moved Temporarily. Redirecting to /?error=invalid%20or%20empty%20fields%20in%20form') .expect(302, done) }) test('play-notify-form-nick-is-lowercased', function(done) { var notify , start = Date.now() this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { foundCb("T") } this.notifier.notify = function(nick, token, data) { notify = [nick, token, data] } request(this.app) .post(PLAY_NOTIFY_FORM) .type('form') .send({nick: "N", message: 'foo'}) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Moved Temporarily. Redirecting to /') .expect(302, function(err) { assert.equal(notify[2]["message"]["to"], "n") done(err) } ) }) test('play-notify-disabled-notifications', function(done) { var notify , start = Date.now() this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { foundCb("T") } this.notifier.notify = function(nick, token, data) { notify = [nick, token, data] } request(this.app) .post(PLAY_NOTIFY_FORM) .type('form') .send({nick: "N", message: 'foo', persist: 'on'}) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Moved Temporarily. Redirecting to /') .expect(302, function(err) { assert.deepEqual(notify[2]["notification"], {}) done(err) } ) }) test('play-notify-enabled-notifications', function(done) { var notify , start = Date.now() this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { foundCb("T") } this.notifier.notify = function(nick, token, data) { notify = [nick, token, data] } request(this.app) .post(PLAY_NOTIFY_FORM) .type('form') .send({nick: "N", message: 'foo', enable: 'on', persist: 'on'}) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Moved Temporarily. Redirecting to /') .expect(302, function(err) { assert.deepEqual(notify[2]["notification"], { card: { summary: 'The website says:', body: 'foo', actions: [ 'appid://com.ubuntu.developer.ralsina.hello/hello/current-user-version' ], persist: true }}) done(err) } ) }) test('play-notify-enabled-all-notifications', function(done) { var notify , start = Date.now() this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { foundCb("T") } this.notifier.notify = function(nick, token, data) { notify = [nick, token, data] } request(this.app) .post(PLAY_NOTIFY_FORM) .type('form') .send({ nick: "N", message: 'foo', enable: 'on', popup: 'on', persist: 'on', sound: 'on', vibrate: 'on', counter: 42 }) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Moved Temporarily. Redirecting to /') .expect(302, function(err) { assert.deepEqual(notify[2]["notification"], { card: { summary: 'The website says:', body: 'foo', actions: [ 'appid://com.ubuntu.developer.ralsina.hello/hello/current-user-version' ], popup: true, persist: true }, sound: true, vibrate: { duration: 200 }, 'emblem-counter': { count: 42, visible: true} }) done(err) } ) }) }) suite('app-with-no-inbox', function() { setup(function() { this.db = {} var cfg = cloneCfg() cfg.no_inbox = true this.app = app.wire(this.db, cfg) this.reg = this.app.get('_reg') this.inbox = this.app.get('_inbox') this.notifier = this.app.get('_notifier') }) test('no-inbox', function() { assert.equal(this.inbox, null) }) test('message-no-inbox', function(done) { var lookup = [] var notify , start = Date.now() this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) { lookup.push(nick) if (nick == "N") { foundCb("T") } else { foundCb("T2") } } this.notifier.notify = function(nick, token, data) { notify = [nick, token, data] } request(this.app) .post('/message') .set('Content-Type', 'application/json') .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"}) .expect('Content-Type', 'application/json; charset=utf-8') .expect({ok: true}) .expect(200, function(err) { assert.deepEqual(lookup, ["N", "N2"]) assert.deepEqual(notify.slice(0, 2), ["N2", "T2"]) var data = notify[2] assert.equal(typeof(data._ephemeral), "number") assert.ok(data._ephemeral >= start) delete data._ephemeral assert.deepEqual(data, {"m": 1, _from:"N"}) done(err) }) }) test('drain-not-there', function(done) { request(this.app) .post('/drain') .set('Content-Type', 'application/json') .send({nick: "N", token: "T", timestamp: 4000}) .expect(404, done) }) }) �������������������������������������ubuntu-push-0.68+16.04.20160310.2/docs/example-server/test/registry_test.js�������������������������0000644�0000156�0000165�00000012202�12670364255�026565� 0����������������������������������������������������������������������������������������������������ustar �pbuser��������������������������pbgroup�������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������var assert = require('assert') var MongoClient = require('mongodb').MongoClient function unexpected(msg) { assert.ok(false, "unexpected: "+msg) } var Registry = require('../lib/registry') suite('Registry', function(){ setup(function(done) { var self = this MongoClient.connect("mongodb://localhost:27017/pushAppTestDb", function(err, database) { if(err) throw err self.db = database // cleanup self.db.collection('registry').drop(function(err) { if(err && err.errmsg != 'ns not found') throw err done() }) }) }) test('insert-and-find', function(done) { var reg = new Registry(this.db) reg.insertToken("N", "T", function() { reg.findToken("N", function(token) { assert.equal(token, "T") done() }, function() { unexpected("not-found") }, function(err) { unexpected(err) }) }, function() { unexpected("dup") }, function(err) { unexpected(err) }) }) test('find-not-found', function(done) { var reg = new Registry(this.db) reg.findToken("N", function() { unexpected("found") }, function() { done() }, function(err) { unexpected(err) }) }) test('insert-identical-dup', function(done) { var reg = new Registry(this.db) reg.insertToken("N", "T", function() { reg.insertToken("N", "T", function() { done() }, function() { unexpected("dup") }, function(err) { unexpected(err) }) }, function() { unexpected("dup") }, function(err) { unexpected(err) }) }) test('insert-dup', function(done) { var reg = new Registry(this.db) reg.insertToken("N", "T1", function() { reg.insertToken("N", "T2", function() { unexpected("success") }, function() { done() }, function(err) { unexpected(err) }) }, function() { unexpected("dup") }, function(err) { unexpected(err) }) }) test('insert-temp-dup', function(done) { var reg = new Registry(this.db) var findToken = reg.findToken , insertToken = reg.insertToken var notFoundOnce = 0 var insertInvocations = 0 reg.findToken = function(nick, foundCb, notFoundCb, errCb) { if (notFoundOnce == 0) { notFoundOnce++ notFoundCb() return } findToken.call(reg, nick, foundCb, notFoundCb, errCb) } reg.insertToken = function(nick, token, doneCb, dupCb, errCb) { insertInvocations++ insertToken.call(reg, nick, token, doneCb, dupCb, errCb) } reg.insertToken("N", "T1", function() { reg.insertToken("N", "T2", function() { unexpected("success") }, function() { assert.equal(insertInvocations, 3) done() }, function(err) { unexpected(err) }) }, function() { unexpected("dup") }, function(err) { unexpected(err) }) }) test('remove', function(done) { var reg = new Registry(this.db) reg.insertToken("N", "T", function() { reg.removeToken("N", "T", function() { reg.findToken("N", function(token) { unexpected("found") }, function() { done() }, function(err) { unexpected(err) }) }, function(err) { unexpected(err) }) }, function() { unexpected("dup") }, function(err) { unexpected(err) }) }) test('remove-exact', function(done) { var reg = new Registry(this.db) reg.insertToken("N", "T1", function() { reg.removeToken("N", "T2", function() { reg.findToken("N", function(token) { assert.equal(token, "T1") done() }, function() { unexpected("no-found") }, function(err) { unexpected(err) }) }, function(err) { unexpected(err) }) }, function() { unexpected("dup") }, function(err) { unexpected(err) }) }) test('remove-nop', function(done) { var reg = new Registry(this.db) reg.removeToken("N1", "T1", function() { reg.findToken("N1", function(token) { unexpected("found") }, function() { done() }, function(err) { unexpected(err) }) }, function(err) { unexpected(err) }) }) }) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ubuntu-push-0.68+16.04.20160310.2/docs/example-server/server.js�������������������������������������0000644�0000156�0000165�00000001353�12670364255�024212� 0����������������������������������������������������������������������������������������������������ustar �pbuser��������������������������pbgroup�������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* Push Notifications App Server Example */ var wire = require('./app').wire var cfg = require('./config/config') var mongoURL = 'mongodb://' + cfg.mongo_host + ':' + cfg.mongo_port + '/pushApp' var MongoClient = require('mongodb').MongoClient MongoClient.connect(mongoURL, cfg.mongo_opts, function(err, database) { if(err) throw err // wire appplication var app = wire(database, cfg) // log errors app.on('pushError', function(err, resp, body) { console.error('pushError', err, resp, body) }) app.on('mongoError', function(err) { console.error('mongoError', err) }) // connection ready => start app app.listen(cfg.listen_port) console.info("Listening on:", cfg.listen_port) }) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ubuntu-push-0.68+16.04.20160310.2/docs/example-server/lib/������������������������������������������0000755�0000156�0000165�00000000000�12670364532�023110� 5����������������������������������������������������������������������������������������������������ustar �pbuser��������������������������pbgroup�������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ubuntu-push-0.68+16.04.20160310.2/docs/example-server/lib/inbox.js����������������������������������0000644�0000156�0000165�00000002217�12670364255�024571� 0����������������������������������������������������������������������������������������������������ustar �pbuser��������������������������pbgroup�������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* token -> token inbox */ function Inbox(db) { this.db = db } Inbox.prototype.pushMessage = function(token, msg, doneCb, errCb) { if (!msg._timestamp) { var now = Date.now() msg._timestamp = now } this.db.collection('inbox').findAndModify({_id: token}, null, { $push: {msgs: msg}, $inc: {serial: 1}, }, {upsert: true, new: true, fields: {serial: 1}}, function(err, doc) { if (err) { errCb(err) return } msg._serial = doc.serial doneCb(msg) }) } Inbox.prototype.drain = function(token, timestamp, doneCb, errCb) { this.db.collection('inbox').findAndModify({_id: token}, null, { $pull: {msgs: {_timestamp: {$lt: timestamp}}} }, {new: true}, function(err, doc) { if (err) { errCb(err) return } if (!doc) { doneCb([]) return } var serial = doc.serial var msgs = doc.msgs for (var i = msgs.length-1; i >= 0; i--) { msgs[i].serial = serial serial-- } doneCb(msgs) }) } module.exports = Inbox ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ubuntu-push-0.68+16.04.20160310.2/docs/example-server/lib/registry.js�������������������������������0000644�0000156�0000165�00000003125�12670364255�025321� 0����������������������������������������������������������������������������������������������������ustar �pbuser��������������������������pbgroup�������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* nick -> token registry */ function Registry(db) { this.db = db } Registry.prototype.findToken = function(nick, foundCb, notFoundCb, errCb) { var self = this self.db.collection('registry').findOne({_id: nick}, function(err, doc) { if (err) { errCb(err) return } if (doc == null) { notFoundCb() return } foundCb(doc.token) }) } Registry.prototype.insertToken = function(nick, token, doneCb, dupCb, errCb) { var self = this doc = {_id: nick, token: token} self.db.collection('registry').insert(doc, function(err) { if (!err) { doneCb() } else { if (err.code == 11000) { // dup self.findToken(nick, function(token2) { if (token == token2) { // same, idempotent doneCb() return } dupCb() }, function() { // not found, try again self.insertToken(nick, token, doneCb, dupCb, errCb) }, function(err) { errCb(err) }) return } errCb(err) } }) } Registry.prototype.removeToken = function(nick, token, doneCb, errCb) { var self = this doc = {_id: nick, token: token} self.db.collection('registry').remove(doc, function(err) { if (err) { errCb(err) return } doneCb() }) } module.exports = Registry �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ubuntu-push-0.68+16.04.20160310.2/docs/example-server/lib/notifier.js�������������������������������0000644�0000156�0000165�00000005100�12670364255�025263� 0����������������������������������������������������������������������������������������������������ustar �pbuser��������������������������pbgroup�������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* Notifier sends notifications, with some retry support */ var request = require('request') , url = require('url') , EventEmitter = require('events').EventEmitter , util = require('util') function Notifier(baseURL, cfg) { this.baseURL = baseURL this.cfg = cfg this._retrier = null this._retryInterval = 0 this._q = [] } util.inherits(Notifier, EventEmitter) Notifier.prototype._retry = function(nick, token, data, cb, expireOn) { var self = this self._q.push([nick, token, data, cb, expireOn]) if (!self._retrier || self._retryInterval == self.cfg.happy_retry_secs) { clearTimeout(self._retrier) self._retryInterval = self.cfg.retry_secs self._retrier = setTimeout(function() { self._doRetry() }, 1000*self._retryInterval) } } Notifier.prototype._doRetry = function() { var self = this self._retryInterval = 0 self._retrier = null var i = 0 while (self._q.length > 0) { var toRetry = self._q.shift() if (new Date() > toRetry[4]) { // expired continue } self.notify(toRetry[0], toRetry[1], toRetry[2], toRetry[3], toRetry[4]) i++ if (i >= self.cfg.retry_batch) { break } } if (self._q.length) { self._retryInterval = self.cfg.happy_retry_secs self._retrier = setTimeout(function() { self._doRetry() }, 1000*self._retryInterval) } } Notifier.prototype.unknownToken = function(nick, token) { this.emit('unknownToken', nick, token) } Notifier.prototype.pushError = function(err, resp, body) { this.emit('pushError', err, resp, body) } Notifier.prototype.notify = function(nick, token, data, cb, expireOn) { var self = this if (!expireOn) { expireOn = new Date expireOn.setUTCMinutes(expireOn.getUTCMinutes() + self.cfg.expire_mins) } var unicast = { 'appid': self.cfg.app_id, 'expire_on': expireOn.toISOString(), 'token': token, 'data': data } request.post(url.resolve(self.baseURL, 'notify'), {json: unicast}, function(error, resp, body) { if (!error) { if (resp.statusCode == 200) { if (cb) cb() return } else if (resp.statusCode > 500) { self._retry(nick, token, data, cb, expireOn) return } else if (resp.statusCode == 400 && body.error == "unknown-token") { self.unknownToken(nick, token) return } } self.pushError(error, resp, body) }) } module.exports = Notifier ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ubuntu-push-0.68+16.04.20160310.2/docs/example-server/.bzrignore������������������������������������0000644�0000156�0000165�00000000005�12670364255�024341� 0����������������������������������������������������������������������������������������������������ustar �pbuser��������������������������pbgroup�������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������.git ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ubuntu-push-0.68+16.04.20160310.2/docs/example-server/notify-form.html������������������������������0000644�0000156�0000165�00000005033�12670364255�025504� 0����������������������������������������������������������������������������������������������������ustar �pbuser��������������������������pbgroup�������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!doctype html> <html lang="en"> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="//maxcdn.bootstrapcdn.com/bootswatch/3.2.0/united/bootstrap.min.css" rel="stylesheet"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script> <script> function enableControls() { disabled = !($('#enable')[0].checked); $('#popup')[0].disabled=disabled; $('#persist')[0].disabled=disabled; $('#sound')[0].disabled=disabled; $('#vibrate')[0].disabled=disabled; $('#counter')[0].disabled=disabled; }; function showError() { var vars = {}; var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); for(var i = 0; i < hashes.length; i++) { hash = hashes[i].split('='); vars[hash[0]] = decodeURIComponent(hash[1]); } if (vars["error"]) { $("<div class='alert alert-danger' role='alert'>" + vars["error"] + "</div>").insertAfter ($("h1")) } } </script> <title>Send a notification

Send a notification

ubuntu-push-0.68+16.04.20160310.2/docs/example-server/index.html0000644000015600001650000000056512670364255024347 0ustar pbuserpbgroup00000000000000 pushAppServer

Push Notifications Application Server Example

ubuntu-push-0.68+16.04.20160310.2/docs/example-server/config/0000755000015600001650000000000012670364532023607 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/docs/example-server/config/config.js0000644000015600001650000000057512670364255025423 0ustar pbuserpbgroup00000000000000module.exports = config = { "name" : "pushAppServer" ,"app_id" : "appEx" ,"listen_port" : 8000 ,"mongo_host" : "localhost" ,"mongo_port" : 27017 ,"mongo_opts" : {} ,"push_url": "https://push.ubuntu.com" ,"retry_batch": 5 ,"retry_secs" : 30 ,"happy_retry_secs": 5 ,"expire_mins": 120 ,"no_inbox": true ,"play_notify_form": true } ubuntu-push-0.68+16.04.20160310.2/docs/push.svg0000644000015600001650000003541112670364255021111 0ustar pbuserpbgroup00000000000000 image/svg+xml Server Side Client Side Push Client Push Helper Application /notify PushServer App Server PopAll Push Notifications App-specific Ubuntu Push System Application Call ubuntu-push-0.68+16.04.20160310.2/docs/Makefile0000644000015600001650000000020012670364255021035 0ustar pbuserpbgroup00000000000000all: *txt *svg rst2html --link-stylesheet highlevel.txt highlevel.html rst2html --link-stylesheet lowlevel.txt lowlevel.html ubuntu-push-0.68+16.04.20160310.2/bus/0000755000015600001650000000000012670364532017244 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/bus.go0000644000015600001650000000420412670364255020366 0ustar pbuserpbgroup00000000000000/* 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 bus provides a simplified (and more testable?) interface to DBus. package bus // Here we define the Bus itself import ( "launchpad.net/go-dbus/v1" "launchpad.net/ubuntu-push/logger" ) /***************************************************************** * Bus (and its implementation) */ // This is the Bus itself. type Bus interface { String() string Endpoint(Address, logger.Logger) Endpoint } type concreteBus dbus.StandardBus // ensure concreteBus implements Bus var _ Bus = new(concreteBus) // no bus.Bus constructor, just two standard, constant, busses var ( SessionBus Bus = concreteBus(dbus.SessionBus) SystemBus Bus = concreteBus(dbus.SystemBus) ) /* public methods */ func (bus concreteBus) String() string { if bus == concreteBus(dbus.SystemBus) { return "SystemBus" } else { return "SessionBus" } } // Endpoint returns a bus endpoint. func (bus concreteBus) Endpoint(addr Address, log logger.Logger) Endpoint { return newEndpoint(bus, addr, log) } // Args helps build arguments for endpoint Call(). func Args(args ...interface{}) []interface{} { return args } /* private methods */ func (bus concreteBus) dbusType() dbus.StandardBus { return dbus.StandardBus(bus) } /***************************************************************** * Address */ // bus.Address is just a bag of configuration type Address struct { Name string Path string Interface string } var BusDaemonAddress = Address{ dbus.BUS_DAEMON_NAME, string(dbus.BUS_DAEMON_PATH), dbus.BUS_DAEMON_IFACE, } ubuntu-push-0.68+16.04.20160310.2/bus/testing/0000755000015600001650000000000012670364532020721 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/testing/testing_endpoint.go0000644000015600001650000002000712670364255024626 0ustar pbuserpbgroup00000000000000/* 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 // Here, the bus.Endpoint implementation. import ( "errors" "fmt" "launchpad.net/go-dbus/v1" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/testing/condition" "sync" "time" ) type callArgs struct { Member string Args []interface{} } type testingEndpoint struct { dialCond condition.Interface callCond condition.Interface usedLck sync.Mutex used int retvals [][]interface{} watchSources map[string]chan []interface{} watchLck sync.RWMutex callArgs []callArgs callArgsLck sync.RWMutex } // Build a bus.Endpoint that calls OK() on its condition before returning // the provided return values. // // NOTE: Call() always returns the first return value; Watch() will provide // each of them in turn, irrespective of whether Call has been called. func NewMultiValuedTestingEndpoint(dialCond condition.Interface, callCond condition.Interface, retvalses ...[]interface{}) bus.Endpoint { return &testingEndpoint{dialCond: dialCond, callCond: callCond, retvals: retvalses, watchSources: make(map[string]chan []interface{})} } func NewTestingEndpoint(dialCond condition.Interface, callCond condition.Interface, retvals ...interface{}) bus.Endpoint { retvalses := make([][]interface{}, len(retvals)) for i, x := range retvals { retvalses[i] = []interface{}{x} } return &testingEndpoint{dialCond: dialCond, callCond: callCond, retvals: retvalses, watchSources: make(map[string]chan []interface{})} } // If SetWatchSource is called with a non-nil watchSource, it is used // instead of the default timeout and retvals to get values to send // over WatchSignal. Set it to nil again to restore default behaviour. func SetWatchSource(tc bus.Endpoint, member string, watchSource chan []interface{}) { tc.(*testingEndpoint).watchLck.Lock() tc.(*testingEndpoint).watchSources[member] = watchSource tc.(*testingEndpoint).watchLck.Unlock() } // GetCallArgs returns a list of the arguments for each Call() invocation. func GetCallArgs(tc bus.Endpoint) []callArgs { tc.(*testingEndpoint).callArgsLck.RLock() defer tc.(*testingEndpoint).callArgsLck.RUnlock() return tc.(*testingEndpoint).callArgs } type watchCancel struct { done chan struct{} cancelled chan struct{} lck sync.Mutex member string } // this waits for actual cancelllation for test convenience func (wc *watchCancel) Cancel() error { wc.lck.Lock() defer wc.lck.Unlock() if wc.cancelled != nil { close(wc.cancelled) wc.cancelled = nil <-wc.done } return nil } // See Endpoint's WatchSignal. This WatchSignal will check its condition to // decide whether to return an error, or provide each of its return values // or values from the previously set watchSource for member. func (tc *testingEndpoint) WatchSignal(member string, f func(...interface{}), d func()) (bus.Cancellable, error) { if tc.callCond.OK() { cancelled := make(chan struct{}) done := make(chan struct{}) go func() { tc.watchLck.RLock() source := tc.watchSources[member] tc.watchLck.RUnlock() if source == nil { tc.usedLck.Lock() idx := tc.used tc.used++ tc.usedLck.Unlock() source = make(chan []interface{}) go func() { Feed: for _, v := range tc.retvals[idx:] { select { case source <- v: case <-cancelled: break Feed } select { case <-time.After(10 * time.Millisecond): case <-cancelled: break Feed } } close(source) }() } Receive: for { select { case v, ok := <-source: if !ok { break Receive } f(v...) case <-cancelled: break Receive } } d() close(done) }() return &watchCancel{cancelled: cancelled, done: done, member: member}, nil } else { return nil, errors.New("no way") } } // See Endpoint's WatchProperties. func (tc *testingEndpoint) WatchProperties(f func(map[string]dbus.Variant, []string), d func()) (bus.Cancellable, error) { translate := func(vals ...interface{}) { changed := vals[0].(map[string]dbus.Variant) invalidated := vals[1].([]string) f(changed, invalidated) } return tc.WatchSignal("PropertiesChanged", translate, d) } // See Endpoint's Call. This Call will check its condition to decide whether // to return an error, or the first of its return values func (tc *testingEndpoint) Call(member string, args []interface{}, rvs ...interface{}) error { tc.callArgsLck.Lock() defer tc.callArgsLck.Unlock() tc.callArgs = append(tc.callArgs, callArgs{member, args}) if tc.callCond.OK() { expected := len(rvs) var provided int tc.usedLck.Lock() idx := tc.used tc.used++ tc.usedLck.Unlock() if len(tc.retvals) <= idx { if expected != 0 { panic("No return values provided!") } provided = 0 } else { provided = len(tc.retvals[idx]) } if provided != expected { return errors.New("provided/expected return vals mismatch") } if provided != 0 { x := dbus.NewMethodCallMessage("", "", "", "") err := x.AppendArgs(tc.retvals[idx]...) if err != nil { return err } err = x.Args(rvs...) if err != nil { return err } } return nil } else { return errors.New("no way") } } // See Endpoint's GetProperty. This one is just another name for Call. func (tc *testingEndpoint) GetProperty(property string) (interface{}, error) { var res interface{} err := tc.Call(property, bus.Args(), &res) if err != nil { return nil, err } return res, err } // See Endpoint's SetProperty. This one does nothing beyond // registering being called. func (tc *testingEndpoint) SetProperty(property string, suffix string, value interface{}) error { tc.callArgsLck.Lock() defer tc.callArgsLck.Unlock() args := callArgs{ Member: "::SetProperty", Args: []interface{}{property, suffix, value}, } tc.callArgs = append(tc.callArgs, args) return nil } // See Endpoint's Dial. This one will check its dialCondition to // decide whether to return an error or not. func (endp *testingEndpoint) Dial() error { if endp.dialCond.OK() { return nil } else { return errors.New("dialCond said No.") } } // Advanced stringifobabulation func (endp *testingEndpoint) String() string { return fmt.Sprintf("&testingEndpoint{dialCond:(%s) callCond:(%s) retvals:(%#v)", endp.dialCond, endp.callCond, endp.retvals) } // see Endpoint's Close. This one does nothing beyond registering // being called. func (tc *testingEndpoint) Close() { tc.callArgsLck.Lock() defer tc.callArgsLck.Unlock() args := callArgs{Member: "::Close"} tc.callArgs = append(tc.callArgs, args) } func (tc *testingEndpoint) GrabName(allowReplacement bool) <-chan error { tc.callArgsLck.Lock() defer tc.callArgsLck.Unlock() args := callArgs{Member: "::GrabName"} args.Args = append(args.Args, allowReplacement) tc.callArgs = append(tc.callArgs, args) return nil } func (tc *testingEndpoint) WatchMethod(dispatch bus.DispatchMap, suffix string, extra ...interface{}) { tc.callArgsLck.Lock() defer tc.callArgsLck.Unlock() args := callArgs{Member: "::WatchMethod"} args.Args = append(args.Args, dispatch, extra) tc.callArgs = append(tc.callArgs, args) } func (tc *testingEndpoint) Signal(member string, suffix string, args []interface{}) error { tc.callArgsLck.Lock() defer tc.callArgsLck.Unlock() callargs := callArgs{Member: "::Signal"} callargs.Args = append(callargs.Args, member, suffix, args) tc.callArgs = append(tc.callArgs, callargs) return nil } // ensure testingEndpoint implements bus.Endpoint var _ bus.Endpoint = (*testingEndpoint)(nil) ubuntu-push-0.68+16.04.20160310.2/bus/testing/testing_bus.go0000644000015600001650000000402312670364255023577 0ustar pbuserpbgroup00000000000000/* 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 provides an implementation of bus.Bus and bus.Endpoint // suitable for testing. package testing // Here, the bus.Bus implementation. import ( "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/testing/condition" ) /***************************************************************** * TestingBus */ type testingBus struct { endp bus.Endpoint } // Build a bus.Bus that takes a condition to determine whether it should work, // as well as a condition and series of return values for the testing // bus.Endpoint it builds. func NewTestingBus(dialTC condition.Interface, callTC condition.Interface, retvals ...interface{}) bus.Bus { return &testingBus{NewTestingEndpoint(dialTC, callTC, retvals...)} } // Build a bus.Bus that takes a condition to determine whether it should work, // as well as a condition and a series of lists of return values for the // testing bus.Endpoint it builds. func NewMultiValuedTestingBus(dialTC condition.Interface, callTC condition.Interface, retvalses ...[]interface{}) bus.Bus { return &testingBus{NewMultiValuedTestingEndpoint(dialTC, callTC, retvalses...)} } // ensure testingBus implements bus.Interface var _ bus.Bus = &testingBus{} /* public methods */ func (tb *testingBus) Endpoint(info bus.Address, log logger.Logger) bus.Endpoint { return tb.endp } func (tb *testingBus) String() string { return "" } ubuntu-push-0.68+16.04.20160310.2/bus/testing/testing_bus_test.go0000644000015600001650000000425212670364255024642 0ustar pbuserpbgroup00000000000000/* 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 import ( . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/testing/condition" "testing" ) // hook up gocheck func BusTest(t *testing.T) { TestingT(t) } type TestingBusSuite struct{} var _ = Suite(&TestingBusSuite{}) // Test Endpoint() on a working bus returns an endpoint that looks right func (s *TestingBusSuite) TestEndpointWorks(c *C) { addr := bus.Address{"", "", ""} tb := NewTestingBus(condition.Work(true), condition.Work(false), 42, 42, 42) endp := tb.Endpoint(addr, nil) err := endp.Dial() c.Check(err, IsNil) c.Assert(endp, FitsTypeOf, &testingEndpoint{}) c.Check(endp.(*testingEndpoint).callCond.OK(), Equals, false) c.Check(endp.(*testingEndpoint).retvals, HasLen, 3) } // Test Endpoint() on a working "multi-valued" bus returns an endpoint that looks right func (s *TestingBusSuite) TestEndpointMultiValued(c *C) { addr := bus.Address{"", "", ""} tb := NewMultiValuedTestingBus(condition.Work(true), condition.Work(true), []interface{}{42, 17}, []interface{}{42, 17, 13}, []interface{}{42}, ) endpp := tb.Endpoint(addr, nil) err := endpp.Dial() c.Check(err, IsNil) endp, ok := endpp.(*testingEndpoint) c.Assert(ok, Equals, true) c.Check(endp.callCond.OK(), Equals, true) c.Assert(endp.retvals, HasLen, 3) c.Check(endp.retvals[0], HasLen, 2) c.Check(endp.retvals[1], HasLen, 3) c.Check(endp.retvals[2], HasLen, 1) } // Test TestingBus stringifies sanely func (s *TestingBusSuite) TestStringifyBus(c *C) { tb := NewTestingBus(nil, nil) c.Check(tb.String(), Matches, ".*TestingBus.*") } ubuntu-push-0.68+16.04.20160310.2/bus/testing/testing_endpoint_test.go0000644000015600001650000002250112670364255025666 0ustar pbuserpbgroup00000000000000/* 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 import ( "testing" "time" "launchpad.net/go-dbus/v1" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/testing/condition" ) // hook up gocheck func Test(t *testing.T) { TestingT(t) } type TestingEndpointSuite struct{} var _ = Suite(&TestingEndpointSuite{}) // Test that Call() with a positive condition returns the first return value // provided, as advertised. func (s *TestingEndpointSuite) TestCallReturnsFirstRetval(c *C) { var m, n uint32 = 42, 17 endp := NewTestingEndpoint(nil, condition.Work(true), m, n) var r uint32 e := endp.Call("what", bus.Args(), &r) c.Check(e, IsNil) c.Check(r, Equals, m) } // Test the same Call() but with multi-valued endpoint func (s *TestingEndpointSuite) TestMultiValuedCall(c *C) { var m, n uint32 = 42, 17 endp := NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{m}, []interface{}{n}) var r uint32 e := endp.Call("what", bus.Args(), &r) c.Check(e, IsNil) c.Check(r, Equals, m) } // Test that Call() with a negative condition returns an error. func (s *TestingEndpointSuite) TestCallFails(c *C) { endp := NewTestingEndpoint(nil, condition.Work(false)) e := endp.Call("what", bus.Args()) c.Check(e, NotNil) } // Test that Call() with a positive condition and no return values panics with // a helpful message. func (s *TestingEndpointSuite) TestCallPanicsWithNiceMessage(c *C) { endp := NewTestingEndpoint(nil, condition.Work(true)) var x int32 c.Check(func() { endp.Call("", bus.Args(), &x) }, PanicMatches, "No return values provided.*") } // Test that Call() updates callArgs func (s *TestingEndpointSuite) TestCallArgs(c *C) { endp := NewTestingEndpoint(nil, condition.Work(true)) err := endp.Call("what", bus.Args("is", "this", "thing")) c.Assert(err, IsNil) c.Check(GetCallArgs(endp), DeepEquals, []callArgs{{"what", []interface{}{"is", "this", "thing"}}}) } // Test that Call() fails but does not explode when asked to return values // that can't be packed into a dbus message. func (s *TestingEndpointSuite) TestCallFailsOnBadRetval(c *C) { endp := NewTestingEndpoint(nil, condition.Work(true), Equals) var r uint32 e := endp.Call("what", bus.Args(), &r) c.Check(e, NotNil) } // Test that Call() fails but does not explode when given an improper result // destination (one into which the dbus response can't be stuffed). func (s *TestingEndpointSuite) TestCallFailsOnBadArg(c *C) { endp := NewTestingEndpoint(nil, condition.Work(true), 1) r := func() {} e := endp.Call("what", bus.Args(), &r) c.Check(e, NotNil) } // Test that WatchSignal() with a positive condition sends the provided return // values over the channel. func (s *TestingEndpointSuite) TestWatch(c *C) { var m, n uint32 = 42, 17 endp := NewTestingEndpoint(nil, condition.Work(true), m, n) ch := make(chan uint32) w, e := endp.WatchSignal("which", func(us ...interface{}) { ch <- us[0].(uint32) }, func() { close(ch) }) c.Assert(e, IsNil) defer w.Cancel() c.Check(<-ch, Equals, m) c.Check(<-ch, Equals, n) } // Test that WatchSignal() calls the destructor callback when it runs out values func (s *TestingEndpointSuite) TestWatchDestructor(c *C) { endp := NewTestingEndpoint(nil, condition.Work(true)) ch := make(chan uint32) w, e := endp.WatchSignal("what", func(us ...interface{}) {}, func() { close(ch) }) c.Assert(e, IsNil) defer w.Cancel() _, ok := <-ch c.Check(ok, Equals, false) } // Test the endpoint can be closed func (s *TestingEndpointSuite) TestCloser(c *C) { endp := NewTestingEndpoint(nil, condition.Work(true)) endp.Close() c.Check(GetCallArgs(endp), DeepEquals, []callArgs{ { Member: "::Close", Args: nil, }}) } // Test that WatchSignal() with a negative condition returns an error. func (s *TestingEndpointSuite) TestWatchFails(c *C) { endp := NewTestingEndpoint(nil, condition.Work(false)) w, e := endp.WatchSignal("what", func(us ...interface{}) {}, func() {}) c.Check(e, NotNil) c.Check(w, IsNil) } // Test WatchSignal can use a watchSource instead of a timeout and retvals (if // the former is not nil) func (s *TestingEndpointSuite) TestWatchSources(c *C) { watchTicker := make(chan []interface{}, 3) watchTicker <- []interface{}{true} watchTicker <- []interface{}{true} watchTicker <- []interface{}{true} c.Assert(len(watchTicker), Equals, 3) endp := NewTestingEndpoint(nil, condition.Work(true), 0, 0) SetWatchSource(endp, "what", watchTicker) ch := make(chan int) w, e := endp.WatchSignal("what", func(us ...interface{}) {}, func() { close(ch) }) c.Assert(e, IsNil) defer w.Cancel() close(watchTicker) // wait for the destructor to be called select { case <-time.Tick(10 * time.Millisecond): c.Fatal("timed out waiting for close on channel") case <-ch: } // now if all went well, the ticker will have been exhausted. c.Assert(len(watchTicker), Equals, 0) } // Test that WatchSignal() calls the destructor callback when canceled. func (s *TestingEndpointSuite) TestWatchCancel(c *C) { endp := NewTestingEndpoint(nil, condition.Work(true)) ch := make(chan uint32) w, e := endp.WatchSignal("what", func(us ...interface{}) {}, func() { close(ch) }) c.Assert(e, IsNil) defer w.Cancel() SetWatchSource(endp, "what", make(chan []interface{})) w.Cancel() _, ok := <-ch c.Check(ok, Equals, false) } // Tests that GetProperty() works func (s *TestingEndpointSuite) TestGetProperty(c *C) { var m uint32 = 42 endp := NewTestingEndpoint(nil, condition.Work(true), m) v, e := endp.GetProperty("what") c.Check(e, IsNil) c.Check(v, Equals, m) } // Tests that GetProperty() fails, too func (s *TestingEndpointSuite) TestGetPropertyFails(c *C) { endp := NewTestingEndpoint(nil, condition.Work(false)) _, e := endp.GetProperty("what") c.Check(e, NotNil) } // Tests that GetProperty() also fails if it's fed garbage func (s *TestingEndpointSuite) TestGetPropertyFailsGargling(c *C) { endp := NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{}) _, e := endp.GetProperty("what") c.Check(e, NotNil) } // Test that WatchProperties() with a positive condition sends the // provided return values over the channel. func (s *TestingEndpointSuite) TestWatchProperties(c *C) { var m, n int32 = 42, 17 endp := NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{map[string]dbus.Variant{"s": dbus.Variant{m}}, []string{}}, []interface{}{map[string]dbus.Variant{"s": dbus.Variant{n}}, []string{}}, ) ch := make(chan int32) w, e := endp.WatchProperties(func(changed map[string]dbus.Variant, nvalited []string) { ch <- changed["s"].Value.(int32) }, func() { close(ch) }) c.Assert(e, IsNil) defer w.Cancel() c.Check(<-ch, Equals, m) c.Check(<-ch, Equals, n) } // Test Dial() with a non-working bus fails func (s *TestingBusSuite) TestDialNoWork(c *C) { endp := NewTestingEndpoint(condition.Work(false), nil) err := endp.Dial() c.Check(err, NotNil) } // Test testingEndpoints serialize, more or less func (s *TestingBusSuite) TestEndpointString(c *C) { endp := NewTestingEndpoint(condition.Fail2Work(2), nil, "hello there") c.Check(endp.String(), Matches, ".*Still Broken.*hello there.*") } // Test that GrabName updates callArgs func (s *TestingEndpointSuite) TestGrabNameUpdatesCallArgs(c *C) { endp := NewTestingEndpoint(nil, condition.Work(true)) endp.GrabName(false) endp.GrabName(true) c.Check(GetCallArgs(endp), DeepEquals, []callArgs{ { Member: "::GrabName", Args: []interface{}{false}, }, { Member: "::GrabName", Args: []interface{}{true}, }}) } // Test that Signal updates callArgs func (s *TestingEndpointSuite) TestSignalUpdatesCallArgs(c *C) { endp := NewTestingEndpoint(nil, condition.Work(true)) endp.Signal("hello", "", []interface{}{"world"}) endp.Signal("hello", "/potato", []interface{}{"there"}) c.Check(GetCallArgs(endp), DeepEquals, []callArgs{ { Member: "::Signal", Args: []interface{}{"hello", "", []interface{}{"world"}}, }, { Member: "::Signal", Args: []interface{}{"hello", "/potato", []interface{}{"there"}}, }}) } // Test that WatchMethod updates callArgs func (s *TestingEndpointSuite) TestWatchMethodUpdatesCallArgs(c *C) { endp := NewTestingEndpoint(nil, condition.Work(true)) foo := func(string, []interface{}, []interface{}) ([]interface{}, error) { return nil, nil } foomp := bus.DispatchMap{"foo": foo} endp.WatchMethod(foomp, "/*") c.Check(GetCallArgs(endp), DeepEquals, []callArgs{ { Member: "::WatchMethod", Args: []interface{}{foomp, []interface{}(nil)}, }}) } // Test that SetProperty updates callArgs func (s *TestingEndpointSuite) TestSetPropertyUpdatesCallArgs(c *C) { endp := NewTestingEndpoint(nil, condition.Work(true)) endp.SetProperty("prop", "suffix", "value") c.Check(GetCallArgs(endp), DeepEquals, []callArgs{ { Member: "::SetProperty", Args: []interface{}{"prop", "suffix", "value"}, }}) } ubuntu-push-0.68+16.04.20160310.2/bus/windowstack/0000755000015600001650000000000012670364532021601 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/windowstack/windowstack.go0000644000015600001650000000401412670364255024466 0ustar pbuserpbgroup00000000000000/* 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 windowstack retrieves information about the windowstack // using Unity's dbus interface package windowstack import ( "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/logger" ) // Well known address for the WindowStack API var BusAddress bus.Address = bus.Address{ Interface: "com.canonical.Unity.WindowStack", Path: "/com/canonical/Unity/WindowStack", Name: "com.canonical.Unity.WindowStack", } type WindowsInfo struct { WindowId uint32 AppId string // in the form "com.ubuntu.calendar_calendar" or "webbrowser-app" Focused bool Stage uint32 } // WindowStack encapsulates info needed to call out to the WindowStack API type WindowStack struct { bus bus.Endpoint log logger.Logger } // New returns a new WindowStack that'll use the provided bus.Endpoint func New(endp bus.Endpoint, log logger.Logger) *WindowStack { return &WindowStack{endp, log} } // GetWindowStack returns the window stack state func (stack *WindowStack) GetWindowStack() []WindowsInfo { var wstack []WindowsInfo err := stack.bus.Call("GetWindowStack", bus.Args(), &wstack) if err != nil { stack.log.Errorf("GetWindowStack call returned %v", err) } return wstack } func (stack *WindowStack) IsAppFocused(AppId *click.AppId) bool { for _, winfo := range stack.GetWindowStack() { if winfo.Focused && winfo.AppId == AppId.Base() { return true } } return false } ubuntu-push-0.68+16.04.20160310.2/bus/windowstack/windowstack_test.go0000644000015600001650000000544512670364255025536 0ustar pbuserpbgroup00000000000000/* 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 windowstack import ( "testing" . "launchpad.net/gocheck" testibus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/click" clickhelp "launchpad.net/ubuntu-push/click/testing" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) func TestWindowStack(t *testing.T) { TestingT(t) } type stackSuite struct { log *helpers.TestLogger app *click.AppId } var _ = Suite(&stackSuite{}) func (hs *stackSuite) SetUpTest(c *C) { hs.log = helpers.NewTestLogger(c, "debug") hs.app = clickhelp.MustParseAppId("com.example.test_test-app_0") } // Checks that GetWindowStack() actually calls GetWindowStack func (ss *stackSuite) TestGetsWindowStack(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), []WindowsInfo{}) ec := New(endp, ss.log) c.Check(ec.GetWindowStack(), DeepEquals, []WindowsInfo{}) callArgs := testibus.GetCallArgs(endp) c.Assert(callArgs, HasLen, 1) c.Check(callArgs[0].Member, Equals, "GetWindowStack") c.Check(callArgs[0].Args, DeepEquals, []interface{}(nil)) } var isFocusedTests = []struct { expected bool // expected result wstack []WindowsInfo // window stack data }{ { false, []WindowsInfo{}, // No windows }, { true, []WindowsInfo{{0, "com.example.test_test-app", true, 0}}, // Just one window, matching app }, { false, []WindowsInfo{{0, "com.example.test_notest-app", true, 0}}, // Just one window, not matching app }, { true, []WindowsInfo{{0, "com.example.test_notest-app", false, 0}, {0, "com.example.test_test-app", true, 0}}, // Two windows, app focused }, { false, []WindowsInfo{{0, "com.example.test_notest-app", true, 0}, {0, "com.example.test_test-app", false, 0}}, // Two windows, app unfocused }, { true, []WindowsInfo{{0, "com.example.test_notest-app", true, 0}, {0, "com.example.test_test-app", true, 0}}, // Two windows, both focused }, } // Check that if the app is focused, IsAppFocused returns true func (ss *stackSuite) TestIsAppFocused(c *C) { for _, t := range isFocusedTests { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), t.wstack) ec := New(endp, ss.log) c.Check(ec.IsAppFocused(ss.app), Equals, t.expected) } } ubuntu-push-0.68+16.04.20160310.2/bus/polld/0000755000015600001650000000000012670364532020356 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/polld/polld_test.go0000644000015600001650000000432112670364255023060 0ustar pbuserpbgroup00000000000000/* 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 polld import ( "testing" "time" . "launchpad.net/gocheck" testibus "launchpad.net/ubuntu-push/bus/testing" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) // hook up gocheck func TestPolld(t *testing.T) { TestingT(t) } type PdSuite struct { log *helpers.TestLogger } var _ = Suite(&PdSuite{}) func (s *PdSuite) SetUpTest(c *C) { s.log = helpers.NewTestLogger(c, "debug") } func (s *PdSuite) TestPollWorks(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) pd := New(endp, s.log) err := pd.Poll() c.Assert(err, IsNil) args := testibus.GetCallArgs(endp) c.Assert(args, HasLen, 1) c.Check(args[0].Member, Equals, "Poll") c.Check(args[0].Args, IsNil) } func (s *PdSuite) TestPollUnconfigured(c *C) { c.Check(new(polld).Poll(), Equals, ErrUnconfigured) } func (s *PdSuite) TestPollFails(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(false)) pd := New(endp, s.log) c.Check(pd.Poll(), NotNil) } func (s *PdSuite) TestWatchDonesWorks(c *C) { endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{}) pd := New(endp, s.log) ch, err := pd.WatchDones() c.Assert(err, IsNil) select { case b := <-ch: c.Check(b, Equals, true) case <-time.After(100 * time.Millisecond): c.Error("timeout waiting for bool") } select { case b := <-ch: c.Check(b, Equals, false) case <-time.After(100 * time.Millisecond): c.Error("timeout waiting for close") } } func (s *PdSuite) TestWatchDonesUnconfigured(c *C) { _, err := new(polld).WatchDones() c.Check(err, Equals, ErrUnconfigured) } ubuntu-push-0.68+16.04.20160310.2/bus/polld/polld.go0000644000015600001650000000312512670364255022022 0ustar pbuserpbgroup00000000000000/* 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 polld wraps the account-polld dbus interface package polld import ( "errors" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/logger" ) var ( ErrUnconfigured = errors.New("unconfigured.") ) // polld lives on a well-known bus.Address var BusAddress bus.Address = bus.Address{ Interface: "com.ubuntu.AccountPolld", Path: "/com/ubuntu/AccountPolld", Name: "com.ubuntu.AccountPolld", } type Polld interface { Poll() error WatchDones() (<-chan bool, error) } type polld struct { endp bus.Endpoint log logger.Logger } func New(endp bus.Endpoint, log logger.Logger) Polld { return &polld{endp, log} } func (p *polld) Poll() error { if p.endp == nil { return ErrUnconfigured } return p.endp.Call("Poll", nil) } func (p *polld) WatchDones() (<-chan bool, error) { if p.endp == nil { return nil, ErrUnconfigured } ch := make(chan bool) p.endp.WatchSignal("Done", func(...interface{}) { ch <- true }, func() { close(ch) }) return ch, nil } ubuntu-push-0.68+16.04.20160310.2/bus/endpoint.go0000644000015600001650000002560612670364255021426 0ustar pbuserpbgroup00000000000000/* 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 bus // Here we define the Endpoint, which represents the DBus connection itself. import ( "encoding/base64" "errors" "fmt" "launchpad.net/go-dbus/v1" "launchpad.net/ubuntu-push/logger" ) /***************************************************************** * Endpoint (and its implementation) */ type BusMethod func(string, []interface{}, []interface{}) ([]interface{}, error) type DispatchMap map[string]BusMethod // Cancellable can be canceled. type Cancellable interface { Cancel() error } // bus.Endpoint represents the DBus connection itself. type Endpoint interface { GrabName(allowReplacement bool) <-chan error WatchSignal(member string, f func(...interface{}), d func()) (Cancellable, error) WatchMethod(DispatchMap, string, ...interface{}) Signal(string, string, []interface{}) error Call(member string, args []interface{}, rvs ...interface{}) error GetProperty(property string) (interface{}, error) SetProperty(property string, suffix string, value interface{}) error WatchProperties(f func(map[string]dbus.Variant, []string), d func()) (Cancellable, error) Dial() error Close() String() string } type endpoint struct { busT Bus bus *dbus.Connection proxy *dbus.ObjectProxy addr Address log logger.Logger } // constructor func newEndpoint(bus Bus, addr Address, log logger.Logger) *endpoint { return &endpoint{busT: bus, addr: addr, log: log} } // ensure endpoint implements Endpoint var _ Endpoint = (*endpoint)(nil) /* public methods XXX: these are almost entirely untested, as that would need XXX: integration tests we are currently missing. */ // Dial() (re)establishes the connection with dbus // // XXX: mostly untested func (endp *endpoint) Dial() error { bus, err := dbus.Connect(endp.busT.(concreteBus).dbusType()) if err != nil { return err } d := dbus.BusDaemon{bus.Object(dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH)} name := endp.addr.Name hasOwner, err := d.NameHasOwner(name) if err != nil { endp.log.Debugf("unable to determine ownership of %#v: %v", name, err) bus.Close() return err } if !hasOwner { // maybe it's waiting to be activated? names, err := d.ListActivatableNames() if err != nil { endp.log.Debugf("%#v has no owner, and when listing activatable: %v", name, err) bus.Close() return err } found := false for _, name := range names { if name == name { found = true break } } if !found { msg := fmt.Sprintf("%#v has no owner, and not in activatables", name) endp.log.Debugf(msg) bus.Close() return errors.New(msg) } } endp.log.Debugf("%#v dialed in.", name) endp.bus = bus endp.proxy = bus.Object(name, dbus.ObjectPath(endp.addr.Path)) return nil } // WatchSignal() takes a member name, sets up a watch for it (on the name, // path and interface provided when creating the endpoint), and then calls f() // with the unpacked value. If it's unable to set up the watch it returns an // error. If the watch fails once established, d() is called. Typically f() // sends the values over a channel, and d() would close the channel. // // XXX: untested func (endp *endpoint) WatchSignal(member string, f func(...interface{}), d func()) (Cancellable, error) { watch, err := endp.proxy.WatchSignal(endp.addr.Interface, member) if err != nil { endp.log.Debugf("failed to set up the watch: %s", err) return nil, err } go endp.unpackMessages(watch, f, d, member) return watch, nil } // Call() invokes the provided member method (on the name, path and // interface provided when creating the endpoint). args can be built // using bus.Args(...). The return value is unpacked into rvs before being // returned. // // XXX: untested func (endp *endpoint) Call(member string, args []interface{}, rvs ...interface{}) error { msg, err := endp.proxy.Call(endp.addr.Interface, member, args...) if err != nil { return err } err = msg.Args(rvs...) if err != nil { return err } return nil } // GetProperty uses the org.freedesktop.DBus.Properties interface's Get method // to read a given property on the name, path and interface provided when // creating the endpoint. The return value is unpacked into a dbus.Variant, // and its value returned. // // XXX: untested func (endp *endpoint) GetProperty(property string) (interface{}, error) { msg, err := endp.proxy.Call("org.freedesktop.DBus.Properties", "Get", endp.addr.Interface, property) if err != nil { return nil, err } variantvs := endp.unpackOneMsg(msg, property) switch len(variantvs) { default: return nil, fmt.Errorf("too many values in Properties.Get response: %d", len(variantvs)) case 0: return nil, fmt.Errorf("not enough values in Properties.Get response: %d", len(variantvs)) case 1: // carry on } variant, ok := variantvs[0].(*dbus.Variant) if !ok { return nil, fmt.Errorf("response from Properties.Get wasn't a *dbus.Variant") } return variant.Value, nil } // SetProperty calls org.freedesktop.DBus.Properties's Set method // // XXX: untested func (endp *endpoint) SetProperty(property string, suffix string, value interface{}) error { // can't use the pre-existing ObjectProxy for this one proxy := endp.bus.Object(endp.addr.Name, dbus.ObjectPath(endp.addr.Path+suffix)) _, err := proxy.Call("org.freedesktop.DBus.Properties", "Set", endp.addr.Interface, property, value) return err } // WatchProperties() sets up a watch for // org.freedesktop.DBus.Properties PropertiesChanged signal for the // path and interface provided when creating the endpoint, and then // calls f() with the unpacked value. If it's unable to set up the // watch it returns an error. If the watch fails once established, d() // is called. Typically f() sends the values over a channel, and d() // would close the channel. // // XXX: untested func (endp *endpoint) WatchProperties(f func(map[string]dbus.Variant, []string), d func()) (Cancellable, error) { watch, err := endp.proxy.WatchSignal("org.freedesktop.DBus.Properties", "PropertiesChanged") if err != nil { endp.log.Debugf("failed to set up the watch: %s", err) return nil, err } go func() { for { msg, ok := <-watch.C if !ok { break } var intfName string var changed map[string]dbus.Variant var invalidated []string if err := msg.Args(&intfName, &changed, &invalidated); err != nil { endp.log.Errorf("unexpected values from Properties watch") break } if intfName != endp.addr.Interface { // ignore continue } f(changed, invalidated) } endp.log.Debugf("got not-OK from Properties watch") d() }() return watch, nil } // Close the connection to dbus. // // XXX: untested func (endp *endpoint) Close() { if endp.bus != nil { endp.bus.Close() endp.bus = nil endp.proxy = nil } } // String() performs advanced endpoint stringification // // XXX: untested func (endp *endpoint) String() string { return fmt.Sprintf("", endp.bus, endp.addr) } // GrabName() takes over the name on the bus, reporting errors over the // returned channel. // // While the first result will be nil on success, successive results would // typically indicate another process trying to take over the name. // // XXX: untested func (endp *endpoint) GrabName(allowReplacement bool) <-chan error { flags := dbus.NameFlagAllowReplacement | dbus.NameFlagReplaceExisting if !allowReplacement { flags = 0 } return endp.bus.RequestName(endp.addr.Name, flags).C } // Signal() sends out a signal called containing . // // XXX: untested func (endp *endpoint) Signal(member string, suffix string, args []interface{}) error { path := dbus.ObjectPath(endp.addr.Path + suffix) msg := dbus.NewSignalMessage(path, endp.addr.Interface, member) if args != nil { err := msg.AppendArgs(args...) if err != nil { endp.log.Errorf("unable to build dbus signal message: %v", err) return err } } err := endp.bus.Send(msg) if err != nil { endp.log.Errorf("unable to send dbus signal: %v", err) } else { endp.log.Debugf("sent dbus signal %s(%#v)", member, args) } return nil } // WatchMethod() uses the given DispatchMap to answer incoming method // calls. // // XXX: untested func (endp *endpoint) WatchMethod(dispatch DispatchMap, suffix string, extra ...interface{}) { ch := make(chan *dbus.Message) go func() { var reply *dbus.Message err_iface := endp.addr.Interface + ".Error" for msg := range ch { meth, ok := dispatch[msg.Member] if !ok || msg.Interface != endp.addr.Interface { reply = dbus.NewErrorMessage(msg, "org.freedesktop.DBus.Error.UnknownMethod", "Unknown method") endp.log.Errorf("WatchMethod: unknown method %s", msg.Member) } else { args := msg.AllArgs() rvals, err := meth(string(msg.Path), args, extra) if err != nil { reply = dbus.NewErrorMessage(msg, err_iface, err.Error()) endp.log.Errorf("WatchMethod: %s(%v, %#v, %#v) failure: %#v", msg.Member, msg.Path, args, extra, err) } else { var san_rvals []string for _, element := range rvals { sane := fmt.Sprintf("%v", element) _, err := base64.StdEncoding.DecodeString(sane) if err == nil { sane = "LooksLikeAToken==" } san_rvals = append(san_rvals, sane) } endp.log.Debugf("WatchMethod: %s(%v, %#v, %#v) success: %#v", msg.Member, msg.Path, args, extra, san_rvals) reply = dbus.NewMethodReturnMessage(msg) err = reply.AppendArgs(rvals...) if err != nil { endp.log.Errorf("WatchMethod: unable to build dbus response message: %v", err) reply = dbus.NewErrorMessage(msg, err_iface, err.Error()) } } } err := endp.bus.Send(reply) if err != nil { endp.log.Errorf("WatchMethod: unable to send reply: %v", err) } } }() path := dbus.ObjectPath(endp.addr.Path + suffix) endp.bus.RegisterObjectPath(path, ch) } /* private methods */ // unpackOneMsg unpacks the value from the response msg // // XXX: untested func (endp *endpoint) unpackOneMsg(msg *dbus.Message, member string) []interface{} { var varmap map[string]dbus.Variant if err := msg.Args(&varmap); err != nil { return msg.AllArgs() } return []interface{}{varmap} } // unpackMessages unpacks the value from the watch // // XXX: untested func (endp *endpoint) unpackMessages(watch *dbus.SignalWatch, f func(...interface{}), d func(), member string) { for { msg, ok := <-watch.C if !ok { break } f(endp.unpackOneMsg(msg, member)...) } endp.log.Debugf("got not-OK from %s watch", member) d() } ubuntu-push-0.68+16.04.20160310.2/bus/powerd/0000755000015600001650000000000012670364532020544 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/powerd/powerd.go0000644000015600001650000000517312670364255022403 0ustar pbuserpbgroup00000000000000/* 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 powerd is an interface to powerd via dbus. package powerd import ( "errors" "time" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/logger" ) // powerd lives on a well-known bus.Address var BusAddress bus.Address = bus.Address{ Interface: "com.canonical.powerd", Path: "/com/canonical/powerd", Name: "com.canonical.powerd", } // Powerd exposes a subset of powerd type Powerd interface { RequestWakeup(name string, wakeupTime time.Time) (string, error) ClearWakeup(cookie string) error WatchWakeups() (<-chan bool, error) RequestWakelock(name string) (string, error) ClearWakelock(cookie string) error } type powerd struct { endp bus.Endpoint log logger.Logger } var ( ErrUnconfigured = errors.New("unconfigured.") ) // New builds a new Powerd that uses the provided bus.Endpoint func New(endp bus.Endpoint, log logger.Logger) Powerd { return &powerd{endp, log} } func (p *powerd) RequestWakeup(name string, wakeupTime time.Time) (string, error) { if p.endp == nil { return "", ErrUnconfigured } var res string err := p.endp.Call("requestWakeup", bus.Args(name, uint64(wakeupTime.Unix())), &res) return res, err } func (p *powerd) ClearWakeup(cookie string) error { if p.endp == nil { return ErrUnconfigured } return p.endp.Call("clearWakeup", bus.Args(cookie)) } func (p *powerd) WatchWakeups() (<-chan bool, error) { if p.endp == nil { return nil, ErrUnconfigured } ch := make(chan bool) p.endp.WatchSignal("Wakeup", func(...interface{}) { ch <- true }, func() { close(ch) }) return ch, nil } func (p *powerd) RequestWakelock(name string) (string, error) { // wakelocks are documented on https://wiki.ubuntu.com/powerd#API // (requestSysState with state=1) if p.endp == nil { return "", ErrUnconfigured } var res string err := p.endp.Call("requestSysState", bus.Args(name, int32(1)), &res) return res, err } func (p *powerd) ClearWakelock(cookie string) error { if p.endp == nil { return ErrUnconfigured } return p.endp.Call("clearSysState", bus.Args(cookie)) } ubuntu-push-0.68+16.04.20160310.2/bus/powerd/powerd_test.go0000644000015600001650000001127112670364255023436 0ustar pbuserpbgroup00000000000000/* 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 powerd import ( "testing" "time" . "launchpad.net/gocheck" testibus "launchpad.net/ubuntu-push/bus/testing" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) // hook up gocheck func TestPowerd(t *testing.T) { TestingT(t) } type PdSuite struct { log *helpers.TestLogger } var _ = Suite(&PdSuite{}) func (s *PdSuite) SetUpTest(c *C) { s.log = helpers.NewTestLogger(c, "debug") } func (s *PdSuite) TestRequestWakeupWorks(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), "cookie") pd := New(endp, s.log) t := time.Now().Add(5 * time.Minute) ck, err := pd.RequestWakeup("name", t) c.Assert(err, IsNil) c.Check(ck, Equals, "cookie") args := testibus.GetCallArgs(endp) c.Assert(args, HasLen, 1) c.Check(args[0].Member, Equals, "requestWakeup") c.Check(args[0].Args, DeepEquals, []interface{}{"name", uint64(t.Unix())}) } func (s *PdSuite) TestRequestWakeupUnconfigured(c *C) { _, err := new(powerd).RequestWakeup("name", time.Now()) c.Assert(err, Equals, ErrUnconfigured) } func (s *PdSuite) TestRequestWakeupFails(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(false)) pd := New(endp, s.log) t := time.Now().Add(5 * time.Minute) _, err := pd.RequestWakeup("name", t) c.Assert(err, NotNil) } func (s *PdSuite) TestClearWakeupWorks(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) pd := New(endp, s.log) err := pd.ClearWakeup("cookie") c.Assert(err, IsNil) args := testibus.GetCallArgs(endp) c.Assert(args, HasLen, 1) c.Check(args[0].Member, Equals, "clearWakeup") c.Check(args[0].Args, DeepEquals, []interface{}{"cookie"}) } func (s *PdSuite) TestClearWakeupUnconfigured(c *C) { err := new(powerd).ClearWakeup("cookie") c.Assert(err, Equals, ErrUnconfigured) } func (s *PdSuite) TestClearWakeupFails(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(false)) pd := New(endp, s.log) err := pd.ClearWakeup("cookie") c.Assert(err, NotNil) } func (s *PdSuite) TestRequestWakelockWorks(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), "cookie") pd := New(endp, s.log) ck, err := pd.RequestWakelock("name") c.Assert(err, IsNil) c.Check(ck, Equals, "cookie") args := testibus.GetCallArgs(endp) c.Assert(args, HasLen, 1) // wakelocks are documented on https://wiki.ubuntu.com/powerd#API c.Check(args[0].Member, Equals, "requestSysState") c.Check(args[0].Args, DeepEquals, []interface{}{"name", int32(1)}) } func (s *PdSuite) TestRequestWakelockUnconfigured(c *C) { _, err := new(powerd).RequestWakelock("name") c.Assert(err, Equals, ErrUnconfigured) } func (s *PdSuite) TestRequestWakelockFails(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(false)) pd := New(endp, s.log) _, err := pd.RequestWakelock("name") c.Assert(err, NotNil) } func (s *PdSuite) TestClearWakelockWorks(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) pd := New(endp, s.log) err := pd.ClearWakelock("cookie") c.Assert(err, IsNil) args := testibus.GetCallArgs(endp) c.Assert(args, HasLen, 1) c.Check(args[0].Member, Equals, "clearSysState") c.Check(args[0].Args, DeepEquals, []interface{}{"cookie"}) } func (s *PdSuite) TestClearWakelockUnconfigured(c *C) { c.Check(new(powerd).ClearWakelock("cookie"), NotNil) } func (s *PdSuite) TestClearWakelockFails(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(false)) pd := New(endp, s.log) err := pd.ClearWakelock("cookie") c.Assert(err, NotNil) } func (s *PdSuite) TestWatchWakeupsWorks(c *C) { endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{}) pd := New(endp, s.log) ch, err := pd.WatchWakeups() c.Assert(err, IsNil) select { case b := <-ch: c.Check(b, Equals, true) case <-time.After(100 * time.Millisecond): c.Error("timeout waiting for bool") } select { case b := <-ch: c.Check(b, Equals, false) case <-time.After(100 * time.Millisecond): c.Error("timeout waiting for close") } } func (s *PdSuite) TestWatchWakeupsUnconfigured(c *C) { _, err := new(powerd).WatchWakeups() c.Check(err, Equals, ErrUnconfigured) } ubuntu-push-0.68+16.04.20160310.2/bus/networkmanager/0000755000015600001650000000000012670364532022270 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/networkmanager/networkmanager.go0000644000015600001650000001323112670364255025645 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2015 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 networkmanager wraps a couple of NetworkManager's DBus API // points: the org.freedesktop.NetworkManager.state call, and // listening for the StateChange signal, similarly for the primary // connection and wireless enabled state. package networkmanager import ( "launchpad.net/go-dbus/v1" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/logger" ) // NetworkManager lives on a well-knwon bus.Address var BusAddress bus.Address = bus.Address{ Interface: "org.freedesktop.NetworkManager", Path: "/org/freedesktop/NetworkManager", Name: "org.freedesktop.NetworkManager", } /***************************************************************** * NetworkManager (and its implementation) */ type NetworkManager interface { // GetState fetches and returns NetworkManager's current state GetState() State // WatchState listens for changes to NetworkManager's state, and sends // them out over the channel returned. WatchState() (<-chan State, bus.Cancellable, error) // GetPrimaryConnection fetches and returns NetworkManager's current // primary connection. GetPrimaryConnection() string // WatchPrimaryConnection listens for changes of NetworkManager's // Primary Connection, and sends them out over the channel returned. WatchPrimaryConnection() (<-chan string, bus.Cancellable, error) // GetWirelessEnabled fetches and returns NetworkManager's // wireless state. GetWirelessEnabled() bool // WatchWirelessEnabled listens for changes of NetworkManager's // wireless state, and sends them out over the channel returned. WatchWirelessEnabled() (<-chan bool, bus.Cancellable, error) } type networkManager struct { bus bus.Endpoint log logger.Logger } // New returns a new NetworkManager that'll use the provided bus.Endpoint func New(endp bus.Endpoint, log logger.Logger) NetworkManager { return &networkManager{endp, log} } // ensure networkManager implements NetworkManager var _ NetworkManager = &networkManager{} /* public methods */ func (nm *networkManager) GetState() State { s, err := nm.bus.GetProperty("state") if err != nil { nm.log.Errorf("failed getting current state: %s", err) nm.log.Debugf("defaulting state to Unknown") return Unknown } v, ok := s.(uint32) if !ok { nm.log.Errorf("got weird state: %#v", s) return Unknown } return State(v) } func (nm *networkManager) WatchState() (<-chan State, bus.Cancellable, error) { ch := make(chan State) w, err := nm.bus.WatchSignal("StateChanged", func(ns ...interface{}) { stint, ok := ns[0].(uint32) if !ok { nm.log.Errorf("got weird state: %#v", ns[0]) return } st := State(stint) nm.log.Debugf("got state: %s", st) ch <- State(stint) }, func() { close(ch) }) if err != nil { nm.log.Debugf("Failed to set up the watch: %s", err) return nil, nil, err } return ch, w, nil } func (nm *networkManager) GetPrimaryConnection() string { got, err := nm.bus.GetProperty("PrimaryConnection") if err != nil { nm.log.Errorf("failed getting current PrimaryConnection: %s", err) nm.log.Debugf("defaulting PrimaryConnection to empty") return "" } v, ok := got.(dbus.ObjectPath) if !ok { nm.log.Errorf("got weird PrimaryConnection: %#v", got) return "" } return string(v) } func (nm *networkManager) WatchPrimaryConnection() (<-chan string, bus.Cancellable, error) { ch := make(chan string) w, err := nm.bus.WatchSignal("PropertiesChanged", func(ppsi ...interface{}) { pps, ok := ppsi[0].(map[string]dbus.Variant) if !ok { nm.log.Errorf("got weird PropertiesChanged: %#v", ppsi[0]) return } v, ok := pps["PrimaryConnection"] if !ok { return } con, ok := v.Value.(dbus.ObjectPath) if !ok { nm.log.Errorf("got weird PrimaryConnection via PropertiesChanged: %#v", v) return } nm.log.Debugf("got PrimaryConnection change: %s", con) ch <- string(con) }, func() { close(ch) }) if err != nil { nm.log.Debugf("failed to set up the watch: %s", err) return nil, nil, err } return ch, w, nil } func (nm *networkManager) GetWirelessEnabled() bool { got, err := nm.bus.GetProperty("WirelessEnabled") if err != nil { nm.log.Errorf("failed getting WirelessEnabled: %s", err) nm.log.Debugf("defaulting WirelessEnabled to true") return true } v, ok := got.(bool) if !ok { nm.log.Errorf("got weird WirelessEnabled: %#v", got) return true } return v } func (nm *networkManager) WatchWirelessEnabled() (<-chan bool, bus.Cancellable, error) { ch := make(chan bool) w, err := nm.bus.WatchSignal("PropertiesChanged", func(ppsi ...interface{}) { pps, ok := ppsi[0].(map[string]dbus.Variant) if !ok { nm.log.Errorf("got weird PropertiesChanged: %#v", ppsi[0]) return } v, ok := pps["WirelessEnabled"] if !ok { return } en, ok := v.Value.(bool) if !ok { nm.log.Errorf("got weird WirelessEnabled via PropertiesChanged: %#v", v) return } nm.log.Debugf("got WirelessEnabled change: %v", en) ch <- en }, func() { close(ch) }) if err != nil { nm.log.Debugf("failed to set up the watch: %s", err) return nil, nil, err } return ch, w, nil } ubuntu-push-0.68+16.04.20160310.2/bus/networkmanager/state.go0000644000015600001650000000256012670364255023744 0ustar pbuserpbgroup00000000000000/* 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 networkmanager type State uint32 // the NetworkManager states, as per // https://wiki.gnome.org/Projects/NetworkManager/DBusInterface/LatestDBusAPI const ( Unknown State = iota * 10 Asleep Disconnected Disconnecting Connecting ConnectedLocal ConnectedSite ConnectedGlobal _max_state ) var names = map[State]string{ Unknown: "Unknown", Asleep: "Asleep", Disconnected: "Disconnected", Disconnecting: "Disconnecting", Connecting: "Connecting", ConnectedLocal: "Connected Local", ConnectedSite: "Connected Site", ConnectedGlobal: "Connected Global", } // give its state a descriptive stringification func (state State) String() string { if s, ok := names[state]; ok { return s } return names[Unknown] } ubuntu-push-0.68+16.04.20160310.2/bus/networkmanager/networkmanager_test.go0000644000015600001650000002641712670364255026716 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2015 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 networkmanager import ( "testing" "launchpad.net/go-dbus/v1" . "launchpad.net/gocheck" testingbus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/logger" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) // hook up gocheck func Test(t *testing.T) { TestingT(t) } type NMSuite struct { log logger.Logger } var _ = Suite(&NMSuite{}) func (s *NMSuite) SetUpTest(c *C) { s.log = helpers.NewTestLogger(c, "debug") } // TestNames checks that networkmanager.State objects serialize // correctly, to a point. func (s *NMSuite) TestNames(c *C) { var i State for i = 0; i < _max_state; i += 10 { c.Check(names[i], Equals, i.String()) } i = _max_state c.Check(i.String(), Equals, "Unknown") } // TestNew doesn't test much at all. If this fails, all is wrong in the world. func (s *NMSuite) TestNew(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(true)), s.log) c.Check(nm, NotNil) } // GetState returns the right state when everything works func (s *NMSuite) TestGetState(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(true), uint32(ConnectedGlobal)), s.log) state := nm.GetState() c.Check(state, Equals, ConnectedGlobal) } // GetState returns the right state when dbus fails func (s *NMSuite) TestGetStateFail(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(false), uint32(ConnectedGlobal)), s.log) state := nm.GetState() c.Check(state, Equals, Unknown) } // GetState returns the right state when dbus works but delivers rubbish values func (s *NMSuite) TestGetStateRubbishValues(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(true), "Unknown"), s.log) state := nm.GetState() c.Check(state, Equals, Unknown) } // GetState returns the right state when dbus works but delivers a rubbish structure func (s *NMSuite) TestGetStateRubbishStructure(c *C) { nm := New(testingbus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{}), s.log) state := nm.GetState() c.Check(state, Equals, Unknown) } // WatchState sends a stream of States over the channel func (s *NMSuite) TestWatchState(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), uint32(Unknown), uint32(Asleep), uint32(ConnectedGlobal)) nm := New(tc, s.log) ch, w, err := nm.WatchState() c.Assert(err, IsNil) defer w.Cancel() l := []State{<-ch, <-ch, <-ch} c.Check(l, DeepEquals, []State{Unknown, Asleep, ConnectedGlobal}) } // WatchState returns on error if the dbus call fails func (s *NMSuite) TestWatchStateFails(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(false)), s.log) _, _, err := nm.WatchState() c.Check(err, NotNil) } // WatchState calls close on its channel when the watch bails func (s *NMSuite) TestWatchStateClosesOnWatchBail(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true)) nm := New(tc, s.log) ch, w, err := nm.WatchState() c.Assert(err, IsNil) defer w.Cancel() _, ok := <-ch c.Check(ok, Equals, false) } // WatchState survives rubbish values func (s *NMSuite) TestWatchStateSurvivesRubbishValues(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), "a") nm := New(tc, s.log) ch, w, err := nm.WatchState() c.Assert(err, IsNil) defer w.Cancel() _, ok := <-ch c.Check(ok, Equals, false) } // GetPrimaryConnection returns the right state when everything works func (s *NMSuite) TestGetPrimaryConnection(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(true), dbus.ObjectPath("/a/1")), s.log) con := nm.GetPrimaryConnection() c.Check(con, Equals, "/a/1") } // GetPrimaryConnection returns the right state when dbus fails func (s *NMSuite) TestGetPrimaryConnectionFail(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(false)), s.log) con := nm.GetPrimaryConnection() c.Check(con, Equals, "") } // GetPrimaryConnection returns the right state when dbus works but delivers rubbish values func (s *NMSuite) TestGetPrimaryConnectionRubbishValues(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(true), "broken"), s.log) con := nm.GetPrimaryConnection() c.Check(con, Equals, "") } // GetPrimaryConnection returns the right state when dbus works but delivers a rubbish structure func (s *NMSuite) TestGetPrimaryConnectionRubbishStructure(c *C) { nm := New(testingbus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{}), s.log) con := nm.GetPrimaryConnection() c.Check(con, Equals, "") } func mkPriConMap(priCon string) map[string]dbus.Variant { m := make(map[string]dbus.Variant) m["PrimaryConnection"] = dbus.Variant{dbus.ObjectPath(priCon)} return m } // WatchPrimaryConnection sends a stream of Connections over the channel func (s *NMSuite) TestWatchPrimaryConnection(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), mkPriConMap("/a/1"), mkPriConMap("/b/2"), mkPriConMap("/c/3")) nm := New(tc, s.log) ch, w, err := nm.WatchPrimaryConnection() c.Assert(err, IsNil) defer w.Cancel() l := []string{<-ch, <-ch, <-ch} c.Check(l, DeepEquals, []string{"/a/1", "/b/2", "/c/3"}) } // WatchPrimaryConnection returns on error if the dbus call fails func (s *NMSuite) TestWatchPrimaryConnectionFails(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(false)), s.log) _, _, err := nm.WatchPrimaryConnection() c.Check(err, NotNil) } // WatchPrimaryConnection calls close on its channel when the watch bails func (s *NMSuite) TestWatchPrimaryConnectionClosesOnWatchBail(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true)) nm := New(tc, s.log) ch, w, err := nm.WatchPrimaryConnection() c.Assert(err, IsNil) defer w.Cancel() _, ok := <-ch c.Check(ok, Equals, false) } // WatchPrimaryConnection survives rubbish values func (s *NMSuite) TestWatchPrimaryConnectionSurvivesRubbishValues(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), "a") nm := New(tc, s.log) ch, w, err := nm.WatchPrimaryConnection() c.Assert(err, IsNil) defer w.Cancel() _, ok := <-ch c.Check(ok, Equals, false) } // WatchPrimaryConnection ignores non-PrimaryConnection PropertyChanged func (s *NMSuite) TestWatchPrimaryConnectionIgnoresIrrelephant(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), map[string]dbus.Variant{"foo": dbus.Variant{}}, map[string]dbus.Variant{"PrimaryConnection": dbus.Variant{dbus.ObjectPath("42")}}, ) nm := New(tc, s.log) ch, w, err := nm.WatchPrimaryConnection() c.Assert(err, IsNil) defer w.Cancel() v, ok := <-ch c.Check(ok, Equals, true) c.Check(v, Equals, "42") } // WatchPrimaryConnection ignores rubbish PrimaryConnections func (s *NMSuite) TestWatchPrimaryConnectionIgnoresRubbishValues(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), map[string]dbus.Variant{"PrimaryConnection": dbus.Variant{-12}}, map[string]dbus.Variant{"PrimaryConnection": dbus.Variant{dbus.ObjectPath("42")}}, ) nm := New(tc, s.log) ch, w, err := nm.WatchPrimaryConnection() c.Assert(err, IsNil) defer w.Cancel() v, ok := <-ch c.Check(ok, Equals, true) c.Check(v, Equals, "42") } // GetWirelessEnabled returns the right state when everything works func (s *NMSuite) TestGetWirelessEnabled(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(true), false), s.log) en := nm.GetWirelessEnabled() c.Check(en, Equals, false) } // GetWirelessEnabled returns the right state when dbus fails func (s *NMSuite) TestGetWirelessEnabledFail(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(false)), s.log) en := nm.GetWirelessEnabled() c.Check(en, Equals, true) } // GetWirelessEnabled returns the right state when dbus works but delivers rubbish values func (s *NMSuite) TestGetWirelessEnabledRubbishValues(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(true), "broken"), s.log) en := nm.GetWirelessEnabled() c.Check(en, Equals, true) } // GetWirelessEnabled returns the right state when dbus works but delivers a rubbish structure func (s *NMSuite) TestGetWirelessEnabledRubbishStructure(c *C) { nm := New(testingbus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{}), s.log) en := nm.GetWirelessEnabled() c.Check(en, Equals, true) } func mkWirelessEnMap(en bool) map[string]dbus.Variant { m := make(map[string]dbus.Variant) m["WirelessEnabled"] = dbus.Variant{en} return m } // WatchWirelessEnabled sends a stream of wireless enabled states over the channel func (s *NMSuite) TestWatchWirelessEnabled(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), mkWirelessEnMap(true), mkWirelessEnMap(false), mkWirelessEnMap(true), ) nm := New(tc, s.log) ch, w, err := nm.WatchWirelessEnabled() c.Assert(err, IsNil) defer w.Cancel() l := []bool{<-ch, <-ch, <-ch} c.Check(l, DeepEquals, []bool{true, false, true}) } // WatchWirelessEnabled returns on error if the dbus call fails func (s *NMSuite) TestWatchWirelessEnabledFails(c *C) { nm := New(testingbus.NewTestingEndpoint(nil, condition.Work(false)), s.log) _, _, err := nm.WatchWirelessEnabled() c.Check(err, NotNil) } // WatchWirelessEnabled calls close on its channel when the watch bails func (s *NMSuite) TestWatchWirelessEnabledClosesOnWatchBail(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true)) nm := New(tc, s.log) ch, w, err := nm.WatchWirelessEnabled() c.Assert(err, IsNil) defer w.Cancel() _, ok := <-ch c.Check(ok, Equals, false) } // WatchWirelessEnabled survives rubbish values func (s *NMSuite) TestWatchWirelessEnabledSurvivesRubbishValues(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), "gorp") nm := New(tc, s.log) ch, w, err := nm.WatchWirelessEnabled() c.Assert(err, IsNil) defer w.Cancel() _, ok := <-ch c.Check(ok, Equals, false) } // WatchWirelessEnabled ignores non-WirelessEnabled PropertyChanged func (s *NMSuite) TestWatchWirelessEnabledIgnoresIrrelephant(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), map[string]dbus.Variant{"foo": dbus.Variant{}}, map[string]dbus.Variant{"WirelessEnabled": dbus.Variant{true}}, ) nm := New(tc, s.log) ch, w, err := nm.WatchWirelessEnabled() c.Assert(err, IsNil) defer w.Cancel() v, ok := <-ch c.Check(ok, Equals, true) c.Check(v, Equals, true) } // WatchWirelessEnabled ignores rubbish WirelessEnabled func (s *NMSuite) TestWatchWirelessEnabledIgnoresRubbishValues(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), map[string]dbus.Variant{"WirelessEnabled": dbus.Variant{-12}}, map[string]dbus.Variant{"WirelessEnabled": dbus.Variant{false}}, ) nm := New(tc, s.log) ch, w, err := nm.WatchWirelessEnabled() c.Assert(err, IsNil) defer w.Cancel() v, ok := <-ch c.Check(ok, Equals, true) c.Check(v, Equals, false) } ubuntu-push-0.68+16.04.20160310.2/bus/unitygreeter/0000755000015600001650000000000012670364532021772 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/unitygreeter/unitygreeter.go0000644000015600001650000000310712670364255025052 0ustar pbuserpbgroup00000000000000/* 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 unitygreeter retrieves information about the Unity Greeter // using Unity's dbus interface package unitygreeter import ( "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/logger" ) // Well known address for the UnityGreeter API var BusAddress bus.Address = bus.Address{ Interface: "com.canonical.UnityGreeter", Path: "/", Name: "com.canonical.UnityGreeter", } // UnityGreeter encapsulates info needed to call out to the UnityGreeter API type UnityGreeter struct { bus bus.Endpoint log logger.Logger } // New returns a new UnityGreeter that'll use the provided bus.Endpoint func New(endp bus.Endpoint, log logger.Logger) *UnityGreeter { return &UnityGreeter{endp, log} } // GetUnityGreeter returns the window stack state func (greeter *UnityGreeter) IsActive() bool { result, err := greeter.bus.GetProperty("IsActive") if err != nil { greeter.log.Errorf("GetUnityGreeter call returned %v", err) return false } return result.(bool) } ubuntu-push-0.68+16.04.20160310.2/bus/bus_test.go0000644000015600001650000000273012670364255021427 0ustar pbuserpbgroup00000000000000/* 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 bus import ( "fmt" "launchpad.net/go-dbus/v1" . "launchpad.net/gocheck" helpers "launchpad.net/ubuntu-push/testing" "testing" ) // hook up gocheck func TestBus(t *testing.T) { TestingT(t) } type BusSuite struct{} var _ = Suite(&BusSuite{}) // Test we stringify sanely func (s *BusSuite) TestBusType(c *C) { c.Check(fmt.Sprintf("%s", SystemBus), Equals, "SystemBus") c.Check(fmt.Sprintf("%s", SessionBus), Equals, "SessionBus") } // Test we get the right DBus bus func (s *BusSuite) TestDBusType(c *C) { c.Check(SystemBus.(concreteBus).dbusType(), DeepEquals, dbus.SystemBus) c.Check(SessionBus.(concreteBus).dbusType(), DeepEquals, dbus.SessionBus) } // Tests that we can get an endpoint back func (s *BusSuite) TestEndpoint(c *C) { endp := SystemBus.Endpoint(Address{"", "", ""}, helpers.NewTestLogger(c, "debug")) c.Assert(endp, NotNil) } ubuntu-push-0.68+16.04.20160310.2/bus/accounts/0000755000015600001650000000000012670364532021063 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/accounts/accounts_test.go0000644000015600001650000002152512670364255024277 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2015 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 accounts import ( "errors" "testing" "launchpad.net/go-dbus/v1" . "launchpad.net/gocheck" testibus "launchpad.net/ubuntu-push/bus/testing" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) // hook up gocheck func TestAcc(t *testing.T) { TestingT(t) } type AccSuite struct { log *helpers.TestLogger } var _ = Suite(&AccSuite{}) type TestCancellable struct { canceled bool err error } func (t *TestCancellable) Cancel() error { t.canceled = true return t.err } func (s *AccSuite) SetUpTest(c *C) { s.log = helpers.NewTestLogger(c, "debug") } func (s *AccSuite) TestBusAddressPathUidLoaded(c *C) { c.Check(BusAddress.Path, Matches, `.*\d+`) } func (s *AccSuite) TestCancelCancelsCancellable(c *C) { err := errors.New("cancel error") t := &TestCancellable{err: err} a := New(nil, s.log).(*accounts) a.cancellable = t c.Check(a.Cancel(), Equals, err) c.Check(t.canceled, Equals, true) } func (s *AccSuite) TestStartReportsWatchError(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(false)) a := New(endp, s.log).(*accounts) c.Assert(a, NotNil) err := a.Start() c.Check(err, NotNil) } func (s *AccSuite) TestStartSetsCancellable(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), true) a := New(endp, s.log).(*accounts) c.Assert(a, NotNil) c.Check(a.cancellable, IsNil) err := a.Start() c.Check(err, IsNil) c.Check(a.cancellable, NotNil) a.Cancel() } func (s *AccSuite) TestStartPanicsIfCalledTwice(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), true, true) a := New(endp, s.log).(*accounts) c.Assert(a, NotNil) c.Check(a.cancellable, IsNil) err := a.Start() c.Check(err, IsNil) c.Check(func() { a.startWatch() }, PanicMatches, `.* twice\?`) a.Cancel() } func (s *AccSuite) TestUpdateCallsUpdaters(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), map[string]dbus.Variant{"x": dbus.Variant{"hello"}}) a := New(endp, s.log).(*accounts) c.Assert(a, NotNil) var x dbus.Variant a.updaters = map[string]func(dbus.Variant){ "x": func(v dbus.Variant) { x = v }, } a.update() c.Check(x.Value, Equals, "hello") } func (s *AccSuite) TestUpdateSilentModeBails(c *C) { a := New(nil, s.log).(*accounts) a.updateSilentMode(dbus.Variant{"rubbish"}) c.Check(s.log.Captured(), Matches, `(?ms)ERROR SilentMode needed a bool.`) } func (s *AccSuite) TestUpdateSilentModeWorks(c *C) { a := New(nil, s.log).(*accounts) c.Check(a.silent, Equals, false) a.updateSilentMode(dbus.Variant{true}) c.Check(a.silent, Equals, true) } func (s *AccSuite) TestUpdateVibrateBails(c *C) { a := New(nil, s.log).(*accounts) a.updateVibrate(dbus.Variant{"rubbish"}) c.Check(s.log.Captured(), Matches, `(?ms)ERROR IncomingMessageVibrate needed a bool.`) } func (s *AccSuite) TestUpdateVibrateWorks(c *C) { a := New(nil, s.log).(*accounts) c.Check(a.vibrate, Equals, false) a.updateVibrate(dbus.Variant{true}) c.Check(a.vibrate, Equals, true) } func (s *AccSuite) TestUpdateVibrateSilentModeBails(c *C) { a := New(nil, s.log).(*accounts) a.updateVibrateSilentMode(dbus.Variant{"rubbish"}) c.Check(s.log.Captured(), Matches, `(?ms)ERROR IncomingMessageVibrateSilentMode needed a bool.`) } func (s *AccSuite) TestUpdateVibrateSilentModeWorks(c *C) { a := New(nil, s.log).(*accounts) c.Check(a.vibrateSilentMode, Equals, false) a.updateVibrateSilentMode(dbus.Variant{true}) c.Check(a.vibrateSilentMode, Equals, true) } func (s *AccSuite) TestUpdateMessageSoundBails(c *C) { a := New(nil, s.log).(*accounts) a.updateMessageSound(dbus.Variant{42}) c.Check(s.log.Captured(), Matches, `(?ms)ERROR IncomingMessageSound needed a string.`) } func (s *AccSuite) TestUpdateMessageSoundWorks(c *C) { a := New(nil, s.log).(*accounts) c.Check(a.messageSound, Equals, "") a.updateMessageSound(dbus.Variant{"xyzzy"}) c.Check(a.messageSound, Equals, "xyzzy") } func (s *AccSuite) TestUpdateMessageSoundPrunesXDG(c *C) { a := New(nil, s.log).(*accounts) a.updateMessageSound(dbus.Variant{"/usr/share/xyzzy"}) c.Check(a.messageSound, Equals, "xyzzy") } func (s *AccSuite) TestPropsHandler(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(false)) // testing a series of bad args for propsHandler: none, New(endp, s.log).(*accounts).propsHandler() c.Check(s.log.Captured(), Matches, `(?ms).*ERROR PropertiesChanged delivered 0 things.*`) s.log.ResetCapture() // bad type for all, New(endp, s.log).(*accounts).propsHandler(nil, nil, nil) c.Check(s.log.Captured(), Matches, `(?ms).*ERROR PropertiesChanged 1st param not a string.*`) s.log.ResetCapture() // wrong interface, New(endp, s.log).(*accounts).propsHandler("xyzzy", nil, nil) c.Check(s.log.Captured(), Matches, `(?ms).*DEBUG PropertiesChanged for "xyzzy", ignoring\..*`) s.log.ResetCapture() // bad type for 2nd and 3rd, New(endp, s.log).(*accounts).propsHandler(accountsSoundIface, nil, nil) c.Check(s.log.Captured(), Matches, `(?ms).*ERROR PropertiesChanged 2nd param not a map.*`) s.log.ResetCapture() // not-seen-in-the-wild 'changed' argument (first non-error outcome), New(endp, s.log).(*accounts).propsHandler(accountsSoundIface, map[interface{}]interface{}{"x": "y"}, nil) // tracking the update() via the GetAll call it generates (which will fail because of the testibus of Work(false) above) c.Check(s.log.Captured(), Matches, `(?ms).*INFO PropertiesChanged provided 'changed'.*ERROR when calling GetAll.*`) s.log.ResetCapture() // bad type for 3rd (with empty 2nd), New(endp, s.log).(*accounts).propsHandler(accountsSoundIface, map[interface{}]interface{}{}, nil) c.Check(s.log.Captured(), Matches, `(?ms).*ERROR PropertiesChanged 3rd param not a list of properties.*`) s.log.ResetCapture() // bad type for elements of 3rd, New(endp, s.log).(*accounts).propsHandler(accountsSoundIface, map[interface{}]interface{}{}, []interface{}{42}) c.Check(s.log.Captured(), Matches, `(?ms).*ERROR PropertiesChanged 3rd param's only entry not a string.*`) s.log.ResetCapture() // empty 3rd (not an error; hard to test "do ), New(endp, s.log).(*accounts).propsHandler(accountsSoundIface, map[interface{}]interface{}{}, []interface{}{}) c.Check(s.log.Captured(), Matches, `(?ms).*DEBUG PropertiesChanged 3rd param is empty.*`) s.log.ResetCapture() // more than one 2rd (also not an error; again looking at the GetAll failure to confirm update() got called), New(endp, s.log).(*accounts).propsHandler(accountsSoundIface, map[interface{}]interface{}{}, []interface{}{"hi", "there"}) c.Check(s.log.Captured(), Matches, `(?ms).*INFO.* reverting to full update.*ERROR when calling GetAll.*`) s.log.ResetCapture() // bus trouble for a single entry in the 3rd, New(endp, s.log).(*accounts).propsHandler(accountsSoundIface, map[interface{}]interface{}{}, []interface{}{"SilentMode"}) c.Check(s.log.Captured(), Matches, `(?ms).*ERROR when calling Get for SilentMode.*`) s.log.ResetCapture() // and finally, the common case: a single entry in the 3rd param, that gets updated individually. xOuter := dbus.Variant{"x"} a := New(testibus.NewTestingEndpoint(nil, condition.Work(true), xOuter), s.log).(*accounts) called := false a.updaters = map[string]func(dbus.Variant){"xyzzy": func(x dbus.Variant) { c.Check(x, Equals, xOuter) called = true }} a.propsHandler(accountsSoundIface, map[interface{}]interface{}{}, []interface{}{"xyzzy"}) c.Check(called, Equals, true) } func (s *AccSuite) TestSilentMode(c *C) { a := New(nil, s.log).(*accounts) c.Check(a.SilentMode(), Equals, false) a.silent = true c.Check(a.SilentMode(), Equals, true) } func (s *AccSuite) TestVibrate(c *C) { a := New(nil, s.log).(*accounts) c.Check(a.Vibrate(), Equals, false) a.vibrate = true c.Check(a.Vibrate(), Equals, true) a.silent = true c.Check(a.Vibrate(), Equals, false) a.vibrateSilentMode = true c.Check(a.Vibrate(), Equals, true) a.vibrate = false c.Check(a.Vibrate(), Equals, true) } func (s *AccSuite) TestMessageSoundFile(c *C) { a := New(nil, s.log).(*accounts) c.Check(a.MessageSoundFile(), Equals, "") a.messageSound = "xyzzy" c.Check(a.MessageSoundFile(), Equals, "xyzzy") } func (s *AccSuite) TestString(c *C) { a := New(nil, s.log).(*accounts) a.vibrate = true a.messageSound = "x" c.Check(a.String(), Equals, `&accounts{silent: false, vibrate: true, vibratesilent: false, messageSound: "x"}`) } ubuntu-push-0.68+16.04.20160310.2/bus/accounts/accounts.go0000644000015600001650000001772212670364255023244 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2015 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 . */ // accounts exposes some properties that're stored in org.freedesktop.Accounts // (specifically, the ones that we need are all under // com.ubuntu.touch.AccountsService.Sound). package accounts import ( "fmt" "os/user" "strings" "sync" "launchpad.net/go-dbus/v1" "launchpad.net/go-xdg/v0" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/logger" ) // accounts lives on a well-known bus.Address. // // Note this one isn't it: the interface is for dbus.properties, and the path // is missing the UID. var BusAddress bus.Address = bus.Address{ Interface: "org.freedesktop.DBus.Properties", Path: "/org/freedesktop/Accounts/User", Name: "org.freedesktop.Accounts", } const accountsSoundIface = "com.ubuntu.touch.AccountsService.Sound" type Accounts interface { // Start() sets up the asynchronous updating of properties, and does the first update. Start() error // Cancel() stops the asynchronous updating of properties. Cancel() error // SilentMode() tells you whether the device is in silent mode. SilentMode() bool // Vibrate() tells you whether the device is allowed to vibrate. Vibrate() bool // MessageSoundFile() tells you the default sound filename. MessageSoundFile() string String() string } // Accounts tracks the relevant bits of configuration. Nothing directly // accessible because it is updated asynchronously, so use the accessors. type accounts struct { endp bus.Endpoint log logger.Logger silent bool vibrate bool vibrateSilentMode bool messageSound string cancellable bus.Cancellable lck sync.Mutex updaters map[string]func(dbus.Variant) } // sets up a new Accounts structure, ready to be Start()ed. func New(endp bus.Endpoint, log logger.Logger) Accounts { a := &accounts{ endp: endp, log: log, } a.updaters = map[string]func(dbus.Variant){ "SilentMode": a.updateSilentMode, "IncomingMessageVibrate": a.updateVibrate, "IncomingMessageVibrateSilentMode": a.updateVibrateSilentMode, "IncomingMessageSound": a.updateMessageSound, } return a } // sets up the asynchronous updating of properties, and does the first update. func (a *accounts) Start() error { err := a.startWatch() if err != nil { return err } a.update() return nil } // does sets up the watch on the PropertiesChanged signal. Separate from Start // because it holds a lock. func (a *accounts) startWatch() error { cancellable, err := a.endp.WatchSignal("PropertiesChanged", a.propsHandler, a.bailoutHandler) if err != nil { a.log.Errorf("unable to watch for property changes: %v", err) return err } a.lck.Lock() defer a.lck.Unlock() if a.cancellable != nil { panic("tried to start Accounts twice?") } a.cancellable = cancellable return nil } // cancel the asynchronous updating of properties. func (a *accounts) Cancel() error { return a.cancellable.Cancel() } // slightly shorter than %#v func (a *accounts) String() string { return fmt.Sprintf("&accounts{silent: %t, vibrate: %t, vibratesilent: %t, messageSound: %q}", a.silent, a.vibrate, a.vibrateSilentMode, a.messageSound) } // merely log that the watch loop has bailed; not much we can do. func (a *accounts) bailoutHandler() { a.log.Debugf("loop bailed out") } // handle PropertiesChanged, which is described in // http://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties func (a *accounts) propsHandler(ns ...interface{}) { if len(ns) != 3 { a.log.Errorf("PropertiesChanged delivered %d things instead of 3.", len(ns)) return } iface, ok := ns[0].(string) if !ok { a.log.Errorf("PropertiesChanged 1st param not a string: %#v.", ns[0]) return } if iface != accountsSoundIface { a.log.Debugf("PropertiesChanged for %#v, ignoring.", iface) return } changed, ok := ns[1].(map[interface{}]interface{}) if !ok { a.log.Errorf("PropertiesChanged 2nd param not a map: %#v.", ns[1]) return } if len(changed) != 0 { // not seen in the wild, but easy to implement properly (ie // using the values we're given) if it starts to // happen. Meanwhile just do a full update. a.log.Infof("PropertiesChanged provided 'changed'; reverting to full update.") a.update() return } invalid, ok := ns[2].([]interface{}) if !ok { a.log.Errorf("PropertiesChanged 3rd param not a list of properties: %#v.", ns[2]) return } a.log.Debugf("props changed: %#v.", invalid) switch len(invalid) { case 0: // nothing to do? a.log.Debugf("PropertiesChanged 3rd param is empty; doing nothing.") case 1: // the common case right now k, ok := invalid[0].(string) if !ok { a.log.Errorf("PropertiesChanged 3rd param's only entry not a string: %#v.", invalid[0]) return } updater, ok := a.updaters[k] if ok { var v dbus.Variant err := a.endp.Call("Get", []interface{}{accountsSoundIface, k}, &v) if err != nil { a.log.Errorf("when calling Get for %s: %v", k, err) return } a.log.Debugf("Get for %s got %#v.", k, v) // updaters must be called with the lock held a.lck.Lock() defer a.lck.Unlock() updater(v) a.log.Debugf("updated %s.", k) } default: // not seen in the wild, but we probably want to drop to a // full update if getting more than one change anyway. a.log.Infof("PropertiesChanged provided more than one 'invalid'; reverting to full update.") a.update() } } func (a *accounts) updateSilentMode(vsilent dbus.Variant) { silent, ok := vsilent.Value.(bool) if !ok { a.log.Errorf("SilentMode needed a bool.") return } a.silent = silent } func (a *accounts) updateVibrate(vvibrate dbus.Variant) { vibrate, ok := vvibrate.Value.(bool) if !ok { a.log.Errorf("IncomingMessageVibrate needed a bool.") return } a.vibrate = vibrate } func (a *accounts) updateVibrateSilentMode(vvibrateSilentMode dbus.Variant) { vibrateSilentMode, ok := vvibrateSilentMode.Value.(bool) if !ok { a.log.Errorf("IncomingMessageVibrateSilentMode needed a bool.") return } a.vibrateSilentMode = vibrateSilentMode } func (a *accounts) updateMessageSound(vsnd dbus.Variant) { snd, ok := vsnd.Value.(string) if !ok { a.log.Errorf("IncomingMessageSound needed a string.") return } for _, dir := range xdg.Data.Dirs()[1:] { if dir[len(dir)-1] != '/' { dir += "/" } if strings.HasPrefix(snd, dir) { snd = snd[len(dir):] break } } a.messageSound = snd } func (a *accounts) update() { props := make(map[string]dbus.Variant) err := a.endp.Call("GetAll", []interface{}{accountsSoundIface}, &props) if err != nil { a.log.Errorf("when calling GetAll: %v", err) return } a.log.Debugf("GetAll got: %#v", props) a.lck.Lock() defer a.lck.Unlock() for name, updater := range a.updaters { updater(props[name]) } } // is the device in silent mode? func (a *accounts) SilentMode() bool { a.lck.Lock() defer a.lck.Unlock() return a.silent } // should notifications vibrate? func (a *accounts) Vibrate() bool { a.lck.Lock() defer a.lck.Unlock() if a.silent { return a.vibrateSilentMode } else { return a.vibrate } } // what is the default sound file? func (a *accounts) MessageSoundFile() string { a.lck.Lock() defer a.lck.Unlock() return a.messageSound } // the BusAddress should actually end with the UID of the user in question; // here we do what's needed to get that. func init() { u, err := user.Current() if err != nil { panic(err) } BusAddress.Path += u.Uid } ubuntu-push-0.68+16.04.20160310.2/bus/notifications/0000755000015600001650000000000012670364532022115 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/notifications/raw.go0000644000015600001650000001324312670364255023242 0ustar pbuserpbgroup00000000000000/* 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 notifications wraps a couple of Notifications's DBus API points: // the org.freedesktop.Notifications.Notify call, and listening for the // ActionInvoked signal. package notifications // this is the lower-level api import ( "encoding/json" "errors" "launchpad.net/go-dbus/v1" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/sounds" ) // Notifications lives on a well-knwon bus.Address var BusAddress bus.Address = bus.Address{ Interface: "org.freedesktop.Notifications", Path: "/org/freedesktop/Notifications", Name: "org.freedesktop.Notifications", } /***************************************************************** * RawNotifications */ // convenience type for the (uint32, string) ActionInvoked signal data type RawAction struct { App *click.AppId `json:"app,omitempty"` Action string `json:"act,omitempty"` ActionId int `json:"aid,omitempty"` Nid string `json:"nid,omitempty"` RawId uint32 `json:"-"` } // a raw notification provides a low-level interface to the f.d.o. dbus // notifications api type RawNotifications struct { bus bus.Endpoint log logger.Logger sound sounds.Sound } // Raw returns a new RawNotifications that'll use the provided bus.Endpoint func Raw(endp bus.Endpoint, log logger.Logger, sound sounds.Sound) *RawNotifications { return &RawNotifications{endp, log, sound} } /* public methods */ // Notify fires a notification func (raw *RawNotifications) Notify( app_name string, reuse_id uint32, icon, summary, body string, actions []string, hints map[string]*dbus.Variant, timeout int32) (uint32, error) { // that's a long argument list! Take a breather. // if raw.bus == nil { return 0, errors.New("unconfigured (missing bus)") } var res uint32 err := raw.bus.Call("Notify", bus.Args(app_name, reuse_id, icon, summary, body, actions, hints, timeout), &res) if err != nil { return 0, err } return res, nil } // WatchActions listens for ActionInvoked signals from the notification daemon // and sends them over the channel provided func (raw *RawNotifications) WatchActions() (<-chan *RawAction, error) { ch := make(chan *RawAction) _, err := raw.bus.WatchSignal("ActionInvoked", func(ns ...interface{}) { if len(ns) != 2 { raw.log.Debugf("ActionInvoked delivered %d things instead of 2", len(ns)) return } rawId, ok := ns[0].(uint32) if !ok { raw.log.Debugf("ActionInvoked's 1st param not a uint32") return } encodedAction, ok := ns[1].(string) if !ok { raw.log.Debugf("ActionInvoked's 2nd param not a string") return } var action *RawAction err := json.Unmarshal([]byte(encodedAction), &action) if err != nil { raw.log.Debugf("ActionInvoked's 2nd param not a json-encoded RawAction") return } action.RawId = rawId ch <- action }, func() { close(ch) }) if err != nil { raw.log.Debugf("failed to set up the watch: %s", err) return nil, err } return ch, nil } // Present displays a given card. // // If card.Actions is empty it's a plain, noninteractive bubble notification. // If card.Actions has 1 action, it's an interactive notification. // If card.Actions has 2 actions, it will show as a snap decision. // If it has more actions, who knows (good luck). func (raw *RawNotifications) Present(app *click.AppId, nid string, notification *launch_helper.Notification) bool { if notification == nil { panic("please check notification is not nil before calling present") } card := notification.Card if card == nil || !card.Popup || card.Summary == "" { raw.log.Debugf("[%s] notification has no popup card: %#v", nid, card) return false } hints := make(map[string]*dbus.Variant) hints["x-canonical-secondary-icon"] = &dbus.Variant{app.SymbolicIcon()} if raw.sound != nil { soundFile := raw.sound.GetSound(app, nid, notification) if soundFile != "" { hints["sound-file"] = &dbus.Variant{soundFile} raw.log.Debugf("[%s] notification will play sound: %s", nid, soundFile) } } appId := app.Original() actions := make([]string, 2*len(card.Actions)) for i, action := range card.Actions { act, err := json.Marshal(&RawAction{ App: app, Nid: nid, ActionId: i, Action: action, }) if err != nil { raw.log.Errorf("[%s] while marshaling %#v to json: %v", nid, action, err) return false } actions[2*i] = string(act) actions[2*i+1] = action } switch len(card.Actions) { case 0: // nothing default: raw.log.Errorf("[%s] don't know what to do with %d actions; ignoring the rest", nid, len(card.Actions)) actions = actions[:2] fallthrough case 1: hints["x-canonical-switch-to-application"] = &dbus.Variant{"true"} } raw.log.Debugf("[%s] creating popup (or snap decision) for %s (summary: %s)", nid, app.Base(), card.Summary) _, err := raw.Notify(appId, 0, card.Icon, card.Summary, card.Body, actions, hints, 30*1000) if err != nil { raw.log.Errorf("[%s] call to Notify failed: %v", nid, err) return false } return true } ubuntu-push-0.68+16.04.20160310.2/bus/notifications/raw_test.go0000644000015600001650000002317112670364255024302 0ustar pbuserpbgroup00000000000000/* 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 notifications wraps a couple of Notifications's DBus API points: // the org.freedesktop.Notifications.Notify call, and listening for the // ActionInvoked signal. package notifications import ( "encoding/json" "testing" "time" "launchpad.net/go-dbus/v1" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/bus" testibus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/click" clickhelp "launchpad.net/ubuntu-push/click/testing" "launchpad.net/ubuntu-push/launch_helper" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) // hook up gocheck func TestRaw(t *testing.T) { TestingT(t) } type RawSuite struct { log *helpers.TestLogger app *click.AppId snd *mockSound } type mockSound struct{} func (m *mockSound) Present(app *click.AppId, nid string, notification *launch_helper.Notification) bool { return false } func (m *mockSound) GetSound(app *click.AppId, nid string, notification *launch_helper.Notification) string { return "/usr/share/sounds/ubuntu/notifications/Xylo.ogg" } func (s *RawSuite) SetUpTest(c *C) { s.log = helpers.NewTestLogger(c, "debug") s.app = clickhelp.MustParseAppId("com.example.test_test-app_0") s.snd = &mockSound{} } var _ = Suite(&RawSuite{}) func (s *RawSuite) TestNotifies(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1)) raw := Raw(endp, s.log, nil) nid, err := raw.Notify("", 0, "", "", "", nil, nil, 0) c.Check(err, IsNil) c.Check(nid, Equals, uint32(1)) } func (s *RawSuite) TestNotifiesFails(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(false)) raw := Raw(endp, s.log, nil) _, err := raw.Notify("", 0, "", "", "", nil, nil, 0) c.Check(err, NotNil) } func (s *RawSuite) TestNotifyFailsIfNoBus(c *C) { raw := Raw(nil, s.log, nil) _, err := raw.Notify("", 0, "", "", "", nil, nil, 0) c.Check(err, ErrorMatches, `.*unconfigured .*`) } func (s *RawSuite) TestNotifiesFailsWeirdly(c *C) { endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{1, 2}) raw := Raw(endp, s.log, nil) _, err := raw.Notify("", 0, "", "", "", nil, nil, 0) c.Check(err, NotNil) } func (s *RawSuite) TestWatchActions(c *C) { act := &RawAction{ App: clickhelp.MustParseAppId("_foo"), Nid: "notif-id", ActionId: 1, Action: "hello", RawId: 0, } jAct, err := json.Marshal(act) c.Assert(err, IsNil) endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{uint32(1), string(jAct)}) raw := Raw(endp, s.log, nil) ch, err := raw.WatchActions() c.Assert(err, IsNil) // check we get the right action reply act.RawId = 1 // checking the RawId is overwritten with the one in the ActionInvoked select { case p := <-ch: c.Check(p, DeepEquals, act) case <-time.NewTimer(time.Second / 10).C: c.Error("timed out?") } // and that the channel is closed if/when the watch fails _, ok := <-ch c.Check(ok, Equals, false) } type tst struct { errstr string endp bus.Endpoint works bool src chan []interface{} } func (s *RawSuite) TestWatchActionsToleratesDBusWeirdness(c *C) { X := func(errstr string, args ...interface{}) tst { endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true)) src := make(chan []interface{}, 1) testibus.SetWatchSource(endp, "ActionInvoked", src) src <- args return tst{errstr, endp, errstr == "", src} } ts := []tst{ X("delivered 0 things instead of 2"), X("delivered 1 things instead of 2", 2), X("1st param not a uint32", 1, "foo"), X("2nd param not a string", uint32(1), nil), X("2nd param not a json-encoded RawAction", uint32(1), ``), X("", uint32(1), `{}`), } for i, t := range ts { raw := Raw(t.endp, s.log, nil) ch, err := raw.WatchActions() c.Assert(err, IsNil) select { case p := <-ch: if !t.works { c.Errorf("got something on the channel! %#v (iter: %d)", p, i) } case <-time.After(time.Second / 10): if t.works { c.Errorf("failed to get something on the channel (iter: %d)", i) } } c.Check(s.log.Captured(), Matches, `(?ms).*`+t.errstr+`.*`) s.log.ResetCapture() close(t.src) } } func (s *RawSuite) TestWatchActionsFails(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(false)) raw := Raw(endp, s.log, nil) _, err := raw.WatchActions() c.Check(err, NotNil) } func (s *RawSuite) TestPresentNotifies(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1)) raw := Raw(endp, s.log, nil) worked := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary", Popup: true}}) c.Check(worked, Equals, true) } func (s *RawSuite) TestPresentWithSoundNotifies(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1)) raw := &RawNotifications{bus: endp, log: s.log, sound: s.snd} id := "notifId" notification := &launch_helper.Notification{ Card: &launch_helper.Card{ Summary: "summary", Popup: true, }, RawSound: json.RawMessage(`true`), } worked := raw.Present(s.app, id, notification) sound := s.snd.GetSound(s.app, id, notification) c.Check(worked, Equals, true) c.Check(s.log.Captured(), Matches, `(?m).*notification will play sound: `+sound+`.*`) } func (s *RawSuite) TestPresentOneAction(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1)) raw := Raw(endp, s.log, nil) worked := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary", Popup: true, Actions: []string{"Yes"}}}) c.Check(worked, Equals, true) callArgs := testibus.GetCallArgs(endp) c.Assert(callArgs, HasLen, 1) c.Assert(callArgs[0].Member, Equals, "Notify") c.Assert(len(callArgs[0].Args), Equals, 8) actions, ok := callArgs[0].Args[5].([]string) c.Assert(ok, Equals, true) c.Assert(actions, HasLen, 2) c.Check(actions[0], Equals, `{"app":"com.example.test_test-app_0","act":"Yes","nid":"notifId"}`) c.Check(actions[1], Equals, "Yes") hints, ok := callArgs[0].Args[6].(map[string]*dbus.Variant) c.Assert(ok, Equals, true) // with one action, there should be 2 hints set: c.Assert(hints, HasLen, 2) c.Check(hints["x-canonical-switch-to-application"], NotNil) c.Check(hints["x-canonical-secondary-icon"], NotNil) c.Check(hints["x-canonical-snap-decisions"], IsNil) c.Check(hints["x-canonical-private-button-tint"], IsNil) } func (s *RawSuite) TestPresentTwoActions(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1)) raw := Raw(endp, s.log, nil) worked := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary", Popup: true, Actions: []string{"Yes", "No"}}}) c.Check(worked, Equals, true) callArgs := testibus.GetCallArgs(endp) c.Assert(callArgs, HasLen, 1) c.Assert(callArgs[0].Member, Equals, "Notify") c.Assert(len(callArgs[0].Args), Equals, 8) actions, ok := callArgs[0].Args[5].([]string) c.Assert(ok, Equals, true) c.Assert(actions, HasLen, 2) c.Check(actions[0], Equals, `{"app":"com.example.test_test-app_0","act":"Yes","nid":"notifId"}`) c.Check(actions[1], Equals, "Yes") // note that the rest are ignored. hints, ok := callArgs[0].Args[6].(map[string]*dbus.Variant) c.Assert(ok, Equals, true) c.Assert(hints, HasLen, 2) c.Check(hints["x-canonical-switch-to-application"], NotNil) c.Check(hints["x-canonical-secondary-icon"], NotNil) } func (s *RawSuite) TestPresentUsesSymbolic(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1)) raw := Raw(endp, s.log, nil) worked := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary", Popup: true}}) c.Assert(worked, Equals, true) callArgs := testibus.GetCallArgs(endp) c.Assert(callArgs, HasLen, 1) c.Assert(callArgs[0].Args, HasLen, 8) hints, ok := callArgs[0].Args[6].(map[string]*dbus.Variant) c.Assert(ok, Equals, true) c.Check(hints["x-canonical-secondary-icon"].Value.(string), Equals, "-symbolic") } func (s *RawSuite) TestPresentNoNotificationPanics(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1)) raw := Raw(endp, s.log, nil) c.Check(func() { raw.Present(s.app, "notifId", nil) }, Panics, `please check notification is not nil before calling present`) } func (s *RawSuite) TestPresentNoCardDoesNotNotify(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1)) raw := Raw(endp, s.log, nil) worked := raw.Present(s.app, "notifId", &launch_helper.Notification{}) c.Check(worked, Equals, false) } func (s *RawSuite) TestPresentNoSummaryDoesNotNotify(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1)) raw := Raw(endp, s.log, nil) worked := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{}}) c.Check(worked, Equals, false) } func (s *RawSuite) TestPresentNoPopupNoNotify(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true), uint32(1)) raw := Raw(endp, s.log, nil) worked := raw.Present(s.app, "notifId", &launch_helper.Notification{Card: &launch_helper.Card{Summary: "summary"}}) c.Check(worked, Equals, false) } ubuntu-push-0.68+16.04.20160310.2/bus/endpoint_test.go0000644000015600001650000000420712670364255022457 0ustar pbuserpbgroup00000000000000/* 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 bus import ( . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/logger" helpers "launchpad.net/ubuntu-push/testing" "os" ) type EndpointSuite struct { log logger.Logger } var _ = Suite(&EndpointSuite{}) func (s *EndpointSuite) SetUpTest(c *C) { s.log = helpers.NewTestLogger(c, "debug") } // TODO: this is going to remain empty until go-dbus grows some // testing amenities (already talked about it with jamesh) // Tests that we can connect to the *actual* system bus. // XXX: maybe connect to a mock/fake/etc bus? func (s *EndpointSuite) TestDial(c *C) { // if somebody's set up the env var, assume it's "live" if os.Getenv("DBUS_SYSTEM_BUS_ADDRESS") == "" { // otherwise, check if _, err := os.Stat("/var/run/dbus/system_bus_socket"); os.IsNotExist(err) { c.Skip("system bus not present") } } endp := newEndpoint(SystemBus, Address{"", "", ""}, s.log) c.Assert(endp.bus, IsNil) err := endp.Dial() c.Assert(err, IsNil) defer endp.Close() // yes, a second close. On purpose. c.Assert(endp.bus, NotNil) endp.Close() // the first close. If you're counting right. c.Assert(endp.bus, IsNil) // Close cleans up } // Test that if we try to connect to the session bus when no session // bus is available, we get a reasonable result (i.e., an error). func (s *EndpointSuite) TestDialCanFail(c *C) { db := "DBUS_SESSION_BUS_ADDRESS" odb := os.Getenv(db) defer os.Setenv(db, odb) os.Setenv(db, "") endp := newEndpoint(SessionBus, Address{"", "", ""}, s.log) err := endp.Dial() c.Check(err, NotNil) } ubuntu-push-0.68+16.04.20160310.2/bus/systemimage/0000755000015600001650000000000012670364532021573 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/systemimage/systemimage_test.go0000644000015600001650000000431312670364270025510 0ustar pbuserpbgroup00000000000000/* 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 systemimage import ( "testing" . "launchpad.net/gocheck" testibus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/logger" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) // hook up gocheck func TestSystemImage(t *testing.T) { TestingT(t) } type SISuite struct { log logger.Logger } var _ = Suite(&SISuite{}) func (s *SISuite) SetUpTest(c *C) { s.log = helpers.NewTestLogger(c, "debug") } func (s *SISuite) TestWorks(c *C) { m := map[string]string{ "version_detail": "ubuntu=20160304.2,device=20160304.2,custom=20160304.2,version=381", "last_update_date": "2016-03-04 15:25:31", "last_check_date": "2016-03-08 04:30:34", "target_version_detail": "-1", "device_name": "mako", "target_build_number": "-1", "channel_name": "ubuntu-touch/rc-proposed/ubuntu", "current_build_number": "381", } endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{m}) si := New(endp, s.log) res, err := si.Information() c.Assert(err, IsNil) c.Check(res, DeepEquals, &InfoResult{ BuildNumber: 381, Device: "mako", Channel: "ubuntu-touch/rc-proposed/ubuntu", LastUpdate: "2016-03-04 15:25:31", VersionDetail: map[string]string{ "ubuntu": "20160304.2", "device": "20160304.2", "custom": "20160304.2", "version": "381", }, Raw: m, }) } func (s *SISuite) TestFailsIfCallFails(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(false)) si := New(endp, s.log) _, err := si.Information() c.Check(err, NotNil) } ubuntu-push-0.68+16.04.20160310.2/bus/systemimage/systemimage.go0000644000015600001650000000525612670364270024460 0ustar pbuserpbgroup00000000000000/* 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 systemimage is a mimimal wrapper for the system-image dbus API. package systemimage import ( "strconv" "strings" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/logger" ) // system-image service lives on a well-known bus.Address var BusAddress bus.Address = bus.Address{ Interface: "com.canonical.SystemImage", Path: "/Service", Name: "com.canonical.SystemImage", } // InfoResult holds the result of the system-image service Info method. type InfoResult struct { BuildNumber int32 Device string Channel string // xxx channel_target missing LastUpdate string VersionDetail map[string]string Raw map[string]string } // A SystemImage exposes the a subset of system-image service. type SystemImage interface { Information() (*InfoResult, error) } type systemImage struct { endp bus.Endpoint log logger.Logger } // New builds a new system-image service wrapper that uses the provided bus.Endpoint func New(endp bus.Endpoint, log logger.Logger) SystemImage { return &systemImage{endp, log} } var _ SystemImage = &systemImage{} // ensures it conforms func (si *systemImage) Information() (*InfoResult, error) { si.log.Debugf("invoking Information") m := map[string]string{} err := si.endp.Call("Information", bus.Args(), &m) if err != nil { si.log.Errorf("Information failed: %v", err) return nil, err } res := &InfoResult{} // Try parsing the build number if it exist. if bn := m["current_build_number"]; len(bn) > 0 { bn, err := strconv.ParseInt(bn, 10, 32) if err == nil { res.BuildNumber = int32(bn) } else { res.BuildNumber = -1 } } res.Device = m["device_name"] res.Channel = m["channel_name"] res.LastUpdate = m["last_update_date"] res.VersionDetail = map[string]string{} // Split version detail key=value,key2=value2 into a string map // Note that even if vals := strings.Split(m["version_detail"], ",") for _, val := range vals { pairs := strings.Split(val, "=") if len(pairs) != 2 { continue } res.VersionDetail[pairs[0]] = pairs[1] } res.Raw = m return res, err } ubuntu-push-0.68+16.04.20160310.2/bus/emblemcounter/0000755000015600001650000000000012670364532022105 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/emblemcounter/emblemcounter_test.go0000644000015600001650000001032612670364255026340 0ustar pbuserpbgroup00000000000000/* 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 emblemcounter import ( "testing" "launchpad.net/go-dbus/v1" . "launchpad.net/gocheck" testibus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/click" clickhelp "launchpad.net/ubuntu-push/click/testing" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/nih" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) func TestEmblemCounter(t *testing.T) { TestingT(t) } type ecSuite struct { log *helpers.TestLogger app *click.AppId } var _ = Suite(&ecSuite{}) func (ecs *ecSuite) SetUpTest(c *C) { ecs.log = helpers.NewTestLogger(c, "debug") ecs.app = clickhelp.MustParseAppId("com.example.test_test-app_0") } // checks that SetCounter() actually calls SetProperty on the launcher func (ecs *ecSuite) TestSetCounterSetsTheCounter(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) quoted := string(nih.Quote([]byte(ecs.app.Base()))) ec := New(endp, ecs.log) c.Check(ec.SetCounter(ecs.app, 42, true), Equals, true) callArgs := testibus.GetCallArgs(endp) c.Assert(callArgs, HasLen, 2) c.Check(callArgs[0].Member, Equals, "::SetProperty") c.Check(callArgs[1].Member, Equals, "::SetProperty") c.Check(callArgs[0].Args, DeepEquals, []interface{}{"count", "/" + quoted, dbus.Variant{Value: int32(42)}}) c.Check(callArgs[1].Args, DeepEquals, []interface{}{"countVisible", "/" + quoted, dbus.Variant{Value: true}}) } // checks that Present() actually calls SetProperty on the launcher func (ecs *ecSuite) TestPresentPresents(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) quoted := string(nih.Quote([]byte(ecs.app.Base()))) ec := New(endp, ecs.log) notif := launch_helper.Notification{EmblemCounter: &launch_helper.EmblemCounter{Count: 42, Visible: true}} c.Check(ec.Present(ecs.app, "nid", ¬if), Equals, true) callArgs := testibus.GetCallArgs(endp) c.Assert(callArgs, HasLen, 2) c.Check(callArgs[0].Member, Equals, "::SetProperty") c.Check(callArgs[1].Member, Equals, "::SetProperty") c.Check(callArgs[0].Args, DeepEquals, []interface{}{"count", "/" + quoted, dbus.Variant{Value: int32(42)}}) c.Check(callArgs[1].Args, DeepEquals, []interface{}{"countVisible", "/" + quoted, dbus.Variant{Value: true}}) } // check that Present() doesn't call SetProperty if no EmblemCounter in the Notification func (ecs *ecSuite) TestSkipIfMissing(c *C) { quoted := string(nih.Quote([]byte(ecs.app.Base()))) endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) ec := New(endp, ecs.log) // nothing happens if no EmblemCounter in Notification c.Check(ec.Present(ecs.app, "nid", &launch_helper.Notification{}), Equals, false) c.Assert(testibus.GetCallArgs(endp), HasLen, 0) // but an empty EmblemCounter is acted on c.Check(ec.Present(ecs.app, "nid", &launch_helper.Notification{EmblemCounter: &launch_helper.EmblemCounter{}}), Equals, true) callArgs := testibus.GetCallArgs(endp) c.Assert(callArgs, HasLen, 2) c.Check(callArgs[0].Member, Equals, "::SetProperty") c.Check(callArgs[1].Member, Equals, "::SetProperty") c.Check(callArgs[0].Args, DeepEquals, []interface{}{"count", "/" + quoted, dbus.Variant{Value: int32(0)}}) c.Check(callArgs[1].Args, DeepEquals, []interface{}{"countVisible", "/" + quoted, dbus.Variant{Value: false}}) } // check that Present() panics if the notification is nil func (ecs *ecSuite) TestPanicsIfNil(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) ec := New(endp, ecs.log) // nothing happens if no EmblemCounter in Notification c.Check(func() { ec.Present(ecs.app, "nid", nil) }, Panics, `please check notification is not nil before calling present`) } ubuntu-push-0.68+16.04.20160310.2/bus/emblemcounter/emblemcounter.go0000644000015600001650000000543412670364255025305 0ustar pbuserpbgroup00000000000000/* 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 emblemcounter can present notifications as a counter on an // emblem on an item in the launcher. package emblemcounter import ( "launchpad.net/go-dbus/v1" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/nih" ) // emblemcounter works by setting properties on a well-known dbus name. var BusAddress = bus.Address{ Interface: "com.canonical.Unity.Launcher.Item", Path: "/com/canonical/Unity/Launcher", Name: "com.canonical.Unity.Launcher", } // EmblemCounter is a little tool that fiddles with the unity launcher // to put emblems with counters on launcher icons. type EmblemCounter struct { bus bus.Endpoint log logger.Logger } // Build an EmblemCounter using the given bus and log. func New(endp bus.Endpoint, log logger.Logger) *EmblemCounter { return &EmblemCounter{bus: endp, log: log} } // Look for an EmblemCounter section in a Notification and, if // present, presents it to the user. func (ctr *EmblemCounter) Present(app *click.AppId, nid string, notification *launch_helper.Notification) bool { if notification == nil { panic("please check notification is not nil before calling present") } ec := notification.EmblemCounter if ec == nil { ctr.log.Debugf("[%s] notification has no EmblemCounter: %#v", nid, ec) return false } ctr.log.Debugf("[%s] setting emblem counter for %s to %d (visible: %t)", nid, app.Base(), ec.Count, ec.Visible) return ctr.SetCounter(app, ec.Count, ec.Visible) } // SetCounter sets an emblem counter on the launcher for app to count (if // visible is true), or clears it (if count is 0 or visible is false). func (ctr *EmblemCounter) SetCounter(app *click.AppId, count int32, visible bool) bool { base := app.Base() quoted := string(nih.Quote([]byte(base))) err := ctr.bus.SetProperty("count", "/"+quoted, dbus.Variant{count}) if err != nil { ctr.log.Errorf("call to set count failed: %v", err) return false } err = ctr.bus.SetProperty("countVisible", "/"+quoted, dbus.Variant{visible}) if err != nil { ctr.log.Errorf("call to set countVisible failed: %v", err) return false } return true } ubuntu-push-0.68+16.04.20160310.2/bus/connectivity/0000755000015600001650000000000012670364532021762 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/connectivity/webchecker.go0000644000015600001650000000517712670364255024427 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2015 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 . */ // webchecker checks whether we're actually connected by doing an http // GET to the Ubuntu connectivity check URL, // http://start.ubuntu.com/connectivity-check.html // // We could make it be https to make extra doubly sure, but it's expensive // overkill for the majority of cases. package connectivity import ( "crypto/md5" "fmt" "io" "time" http13 "launchpad.net/ubuntu-push/http13client" "launchpad.net/ubuntu-push/logger" ) // how much web would a webchecker check type Webchecker interface { // Webcheck checks whether retrieving the URL works, and if its // contents match the target. If so, then it sends true; if anything // fails, it sends false. Webcheck(chan<- bool) // Close idle connections. Close() } type webchecker struct { log logger.Logger url string target string cli *http13.Client } // Build a webchecker for the given URL, that should match the target MD5. func NewWebchecker(url string, target string, timeout time.Duration, log logger.Logger) Webchecker { cli := &http13.Client{ Timeout: timeout, Transport: &http13.Transport{TLSHandshakeTimeout: timeout}, } return &webchecker{log, url, target, cli} } // ensure webchecker implements Webchecker var _ Webchecker = &webchecker{} func (wb *webchecker) Webcheck(ch chan<- bool) { response, err := wb.cli.Get(wb.url) if err != nil { wb.log.Errorf("while GETting %s: %v", wb.url, err) ch <- false return } defer response.Body.Close() hash := md5.New() _, err = io.CopyN(hash, response.Body, 1024) if err != io.EOF { if err == nil { wb.log.Errorf("reading %s, but response body is larger than 1k.", wb.url) } else { wb.log.Errorf("reading %s, expecting EOF, got: %v", wb.url, err) } ch <- false return } sum := fmt.Sprintf("%x", hash.Sum(nil)) if sum == wb.target { wb.log.Infof("connectivity check passed.") ch <- true } else { wb.log.Infof("connectivity check failed: content mismatch.") ch <- false } } func (wb *webchecker) Close() { wb.cli.Transport.(*http13.Transport).CloseIdleConnections() } ubuntu-push-0.68+16.04.20160310.2/bus/connectivity/connectivity.go0000644000015600001650000001706112670364255025036 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2015 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 connectivity implements a single, simple stream of booleans // to answer the question "are we connected?". // // It can potentially fire two falses in a row, if a disconnected // state is followed by a dbus watch error. Other than that, it's edge // triggered. package connectivity import ( "errors" "sync" "time" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/bus/networkmanager" "launchpad.net/ubuntu-push/config" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/util" ) // The configuration for ConnectedState, intended to be populated from a config file. type ConnectivityConfig struct { // how long to wait after a state change to make sure it's "stable" // before acting on it StabilizingTimeout config.ConfigTimeDuration `json:"stabilizing_timeout"` // How long to wait between online connectivity checks. RecheckTimeout config.ConfigTimeDuration `json:"recheck_timeout"` // The URL against which to do the connectivity check. ConnectivityCheckURL string `json:"connectivity_check_url"` // The expected MD5 of the content at the ConnectivityCheckURL ConnectivityCheckMD5 string `json:"connectivity_check_md5"` } // ConnectedState helps tracking connectivity. type ConnectedState struct { networkStateCh <-chan networkmanager.State networkConCh <-chan string config ConnectivityConfig log logger.Logger endp bus.Endpoint connAttempts uint32 webchk Webchecker webgetCh chan bool currentState networkmanager.State lastSent bool timer *time.Timer doneLck sync.Mutex done chan struct{} canceled bool stateWatch bus.Cancellable conWatch bus.Cancellable } // New makes a ConnectedState for connectivity tracking. // // The endpoint need not be dialed; Track() will Dial() and // Close() it as it sees fit. func New(endp bus.Endpoint, config ConnectivityConfig, log logger.Logger) *ConnectedState { wg := NewWebchecker(config.ConnectivityCheckURL, config.ConnectivityCheckMD5, 10*time.Second, log) return &ConnectedState{ config: config, log: log, endp: endp, webchk: wg, done: make(chan struct{}), } } // cancel watches if any func (cs *ConnectedState) reset() { if cs.stateWatch != nil { cs.stateWatch.Cancel() cs.stateWatch = nil } if cs.conWatch != nil { cs.conWatch.Cancel() cs.conWatch = nil } } // start connects to the bus, gets the initial NetworkManager state, and sets // up the watch. func (cs *ConnectedState) start() networkmanager.State { var initial networkmanager.State var stateCh <-chan networkmanager.State var primary string var conCh <-chan string var err error for { ar := util.NewAutoRedialer(cs.endp) cs.connAttempts += ar.Redial() nm := networkmanager.New(cs.endp, cs.log) cs.reset() // set up the watch stateCh, cs.stateWatch, err = nm.WatchState() if err != nil { cs.log.Debugf("failed to set up the state watch: %s", err) goto Continue } // Get the current state. initial = nm.GetState() if initial == networkmanager.Unknown { cs.log.Debugf("failed to get state.") goto Continue } cs.log.Debugf("got initial state of %s", initial) conCh, cs.conWatch, err = nm.WatchPrimaryConnection() if err != nil { cs.log.Debugf("failed to set up the connection watch: %s", err) goto Continue } primary = nm.GetPrimaryConnection() cs.log.Debugf("primary connection starts as %#v", primary) cs.networkStateCh = stateCh cs.networkConCh = conCh return initial Continue: cs.endp.Close() time.Sleep(10 * time.Millisecond) // that should cool things } } var errCanceled = errors.New("canceled") // step takes one step forwards in the “am I connected?†// answering state machine. func (cs *ConnectedState) step() (bool, error) { stabilizingTimeout := cs.config.StabilizingTimeout.Duration recheckTimeout := cs.config.RecheckTimeout.Duration log := cs.log Loop: for { select { case <-cs.done: return false, errCanceled case <-cs.networkConCh: cs.webgetCh = nil cs.timer.Reset(stabilizingTimeout) if cs.lastSent == true { log.Debugf("connectivity: PrimaryConnection changed. lastSent: %v, sending 'disconnected'.", cs.lastSent) cs.lastSent = false break Loop } else { log.Debugf("connectivity: PrimaryConnection changed. lastSent: %v, Ignoring.", cs.lastSent) } case v, ok := <-cs.networkStateCh: // Handle only disconnecting here, connecting handled under the timer below if !ok { // tear it all down and start over return false, errors.New("got not-OK from StateChanged watch") } cs.webgetCh = nil lastState := cs.currentState cs.currentState = v // ignore Connecting (followed immediately by "Connected Global") and repeats if v != networkmanager.Connecting && lastState != v { cs.timer.Reset(stabilizingTimeout) log.Debugf("state changed to %s. Assuming disconnect.", v) if cs.lastSent == true { log.Debugf("connectivity: %s -> %s. lastSent: %v, sending 'disconnected'", lastState, v, cs.lastSent) cs.lastSent = false break Loop } else { log.Debugf("connectivity: %s -> %s. lastSent: %v, Ignoring.", lastState, v, cs.lastSent) } } else { log.Debugf("connectivity: %s -> %s. lastSent: %v, Ignoring.", lastState, v, cs.lastSent) } case <-cs.timer.C: if cs.currentState == networkmanager.ConnectedGlobal { log.Debugf("connectivity: timer signal, state: ConnectedGlobal, checking...") // use a buffered channel, otherwise // we may leak webcheckers that cannot // send their result because we have // cleared webgetCh and wont receive // on it cs.webgetCh = make(chan bool, 1) go cs.webchk.Webcheck(cs.webgetCh) } case connected := <-cs.webgetCh: cs.timer.Reset(recheckTimeout) log.Debugf("connectivity: connection check says: %t", connected) cs.webgetCh = nil if connected && cs.lastSent == false { log.Debugf("connectivity: connection check ok, lastSent: %v, sending 'connected'.", cs.lastSent) cs.lastSent = true break Loop } } } return cs.lastSent, nil } // Track sends the initial NetworkManager state and changes to it // over the "out" channel. Sends "false" as soon as it detects trouble, "true" // after checking actual connectivity. // func (cs *ConnectedState) Track(out chan<- bool) { Start: cs.log.Debugf("sending initial 'disconnected'.") select { case <-cs.done: return case out <- false: } cs.lastSent = false cs.currentState = cs.start() defer cs.reset() cs.timer = time.NewTimer(cs.config.StabilizingTimeout.Duration) for { v, err := cs.step() if err == errCanceled { return } if err != nil { // tear it all down and start over cs.log.Errorf("%s", err) goto Start } select { case <-cs.done: return case out <- v: } } } // Cancel stops the ConnectedState machinary. func (cs *ConnectedState) Cancel() { cs.doneLck.Lock() defer cs.doneLck.Unlock() if !cs.canceled { cs.canceled = true close(cs.done) cs.webchk.Close() } } ubuntu-push-0.68+16.04.20160310.2/bus/connectivity/webchecker_test.go0000644000015600001650000001066012670364255025457 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2015 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 connectivity import ( . "launchpad.net/gocheck" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/util" "net/http" "net/http/httptest" "time" ) type WebcheckerSuite struct { timeouts []time.Duration log *helpers.TestLogger } var _ = Suite(&WebcheckerSuite{}) const ( staticText = "something ipsum dolor something" staticHash = "6155f83b471583f47c99998a472a178f" bigText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus tincidunt vitae sapien tempus fermentum. Cras commodo augue luctu, tempus libero sit amet, laoreet lectus. Vestibulum ali justo et malesuada placerat. Pellentesque viverra luctus velit, adipiscing fermentum tortori vehicula nec. Integer tincidunt purus et pretium vestibulum. Donec portas suscipit pulvinar. Suspendisse potenti. Donec sit amet pharetra nisl, sit amet posuere orci. In feugiat elitist nec augue fringilla, a rutrum risus posuere. Aliquam erat volutpat. Morbi aliquam arcu et eleifend placeraten. Pellentesque egestas varius aliquam. In egestas nisi sed ipsum tristiquer lacinia. Sed vitae nisi non eros consectetur vestibulum vehicularum vitae. Curabitur cursus consectetur eros, in vestibulum turpis cursus at i lorem. Pellentesque ultrices arcu ut massa faucibus, e consequat sapien placerat. Maecenas quis ultricies mi. Phasellus turpis nisl, porttitor ac mi cursus, euismod imperdiet lorem. Donec facilisis est id dignissim imperdiet.` bigHash = "9bf86bce26e8f2d9c9d9bd4a98f9e668" ) // mkHandler makes an http.HandlerFunc that returns the provided text // for whatever request it's given. func mkHandler(text string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.(http.Flusher).Flush() w.Write([]byte(text)) w.(http.Flusher).Flush() } } func (s *WebcheckerSuite) SetUpSuite(c *C) { s.timeouts = util.SwapTimeouts([]time.Duration{0}) } func (s *WebcheckerSuite) TearDownSuite(c *C) { util.SwapTimeouts(s.timeouts) s.timeouts = nil } func (s *WebcheckerSuite) SetUpTest(c *C) { s.log = helpers.NewTestLogger(c, "debug") } // Webchecker sends true when everything works func (s *WebcheckerSuite) TestWorks(c *C) { ts := httptest.NewServer(mkHandler(staticText)) defer ts.Close() ck := NewWebchecker(ts.URL, staticHash, 5*time.Second, s.log) defer ck.Close() ch := make(chan bool, 1) ck.Webcheck(ch) c.Check(<-ch, Equals, true) } // Webchecker sends false if the download fails. func (s *WebcheckerSuite) TestActualFails(c *C) { ck := NewWebchecker("garbage://", "", 5*time.Second, s.log) defer ck.Close() ch := make(chan bool, 1) ck.Webcheck(ch) c.Check(<-ch, Equals, false) } // Webchecker sends false if the hash doesn't match func (s *WebcheckerSuite) TestHashFails(c *C) { ts := httptest.NewServer(mkHandler("")) defer ts.Close() ck := NewWebchecker(ts.URL, staticHash, 5*time.Second, s.log) defer ck.Close() ch := make(chan bool, 1) ck.Webcheck(ch) c.Check(<-ch, Equals, false) c.Check(s.log.Captured(), Matches, "(?ism).*content mismatch.*") } // Webchecker sends false if the download is too big func (s *WebcheckerSuite) TestTooBigFails(c *C) { ts := httptest.NewServer(mkHandler(bigText)) defer ts.Close() ck := NewWebchecker(ts.URL, bigHash, 5*time.Second, s.log) defer ck.Close() ch := make(chan bool, 1) ck.Webcheck(ch) c.Check(<-ch, Equals, false) c.Check(s.log.Captured(), Matches, "(?ism).*larger than 1k.*") } // Webchecker sends false if the request timeouts func (s *WebcheckerSuite) TestTooSlowFails(c *C) { finish := make(chan bool) handler := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { <-finish // get stuck }) ts := httptest.NewServer(handler) defer ts.Close() defer func() { finish <- true }() ck := NewWebchecker(ts.URL, bigHash, time.Second, s.log) defer ck.Close() ch := make(chan bool, 1) ck.Webcheck(ch) c.Check(<-ch, Equals, false) } ubuntu-push-0.68+16.04.20160310.2/bus/connectivity/connectivity_test.go0000644000015600001650000003244312670364255026076 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2015 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 connectivity import ( "net/http/httptest" "sync" "testing" "time" "launchpad.net/go-dbus/v1" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/bus/networkmanager" testingbus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/config" "launchpad.net/ubuntu-push/logger" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" "launchpad.net/ubuntu-push/util" ) // hook up gocheck func Test(t *testing.T) { TestingT(t) } type ConnSuite struct { timeouts []time.Duration log logger.Logger } var _ = Suite(&ConnSuite{}) func (s *ConnSuite) SetUpSuite(c *C) { s.timeouts = util.SwapTimeouts([]time.Duration{0, 0, 0, 0}) } func (s *ConnSuite) TearDownSuite(c *C) { util.SwapTimeouts(s.timeouts) s.timeouts = nil } func (s *ConnSuite) SetUpTest(c *C) { s.log = helpers.NewTestLogger(c, "debug") } var ( helloCon = dbus.ObjectPath("hello") helloConProps = map[string]dbus.Variant{"PrimaryConnection": dbus.Variant{helloCon}} ) /* tests for ConnectedState's Start() method */ // when given a working config and bus, Start() will work func (s *ConnSuite) TestStartWorks(c *C) { endp := testingbus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(networkmanager.Connecting), helloCon) cs := ConnectedState{config: ConnectivityConfig{}, log: s.log, endp: endp} nopTicker := make(chan []interface{}) testingbus.SetWatchSource(endp, "StateChanged", nopTicker) testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker) defer close(nopTicker) c.Check(cs.start(), Equals, networkmanager.Connecting) } // if the bus fails a couple of times, we're still OK func (s *ConnSuite) TestStartRetriesConnect(c *C) { endp := testingbus.NewTestingEndpoint(condition.Fail2Work(2), condition.Work(true), uint32(networkmanager.Connecting), helloCon) cs := ConnectedState{config: ConnectivityConfig{}, log: s.log, endp: endp} nopTicker := make(chan []interface{}) testingbus.SetWatchSource(endp, "StateChanged", nopTicker) testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker) defer close(nopTicker) c.Check(cs.start(), Equals, networkmanager.Connecting) c.Check(cs.connAttempts, Equals, uint32(3)) // 1 more than the Fail2Work } // when the calls to NetworkManager fails for a bit, we're still OK func (s *ConnSuite) TestStartRetriesCall(c *C) { endp := testingbus.NewTestingEndpoint(condition.Work(true), condition.Fail2Work(5), uint32(networkmanager.Connecting), helloCon) cs := ConnectedState{config: ConnectivityConfig{}, log: s.log, endp: endp} nopTicker := make(chan []interface{}) testingbus.SetWatchSource(endp, "StateChanged", nopTicker) testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker) defer close(nopTicker) c.Check(cs.start(), Equals, networkmanager.Connecting) c.Check(cs.connAttempts, Equals, uint32(6)) } // when some of the calls to NetworkManager fails for a bit, we're still OK func (s *ConnSuite) TestStartRetriesCall2(c *C) { cond := condition.Chain(1, condition.Work(true), 1, condition.Work(false), 1, condition.Work(true)) endp := testingbus.NewTestingEndpoint(condition.Work(true), cond, uint32(networkmanager.Connecting), helloCon, uint32(networkmanager.Connecting), helloCon, ) cs := ConnectedState{config: ConnectivityConfig{}, log: s.log, endp: endp} nopTicker := make(chan []interface{}) testingbus.SetWatchSource(endp, "StateChanged", nopTicker) testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker) defer close(nopTicker) c.Check(cs.start(), Equals, networkmanager.Connecting) } // when... and bear with me... the bus works, and the first call to // get network manager's state works, but then you can't establish the // watch, we recover and try again. func (s *ConnSuite) TestStartRetriesWatch(c *C) { nmcond := condition.Chain( 2, condition.Work(true), // 2 call to nm works 1, condition.Work(false), // 1 call to nm fails 0, condition.Work(true)) // and everything works from there on endp := testingbus.NewTestingEndpoint(condition.Work(true), nmcond, uint32(networkmanager.Connecting), uint32(networkmanager.Connecting), helloCon, ) cs := ConnectedState{config: ConnectivityConfig{}, log: s.log, endp: endp} watchTicker := make(chan []interface{}, 1) nopTicker := make(chan []interface{}) testingbus.SetWatchSource(endp, "StateChanged", watchTicker) testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker) defer close(nopTicker) defer close(watchTicker) c.Check(cs.start(), Equals, networkmanager.Connecting) c.Check(cs.connAttempts, Equals, uint32(2)) watchTicker <- []interface{}{uint32(networkmanager.ConnectedGlobal)} c.Check(<-cs.networkStateCh, Equals, networkmanager.ConnectedGlobal) } // a racyEndpoint is an endpoint that behaves differently depending on // how much time passes between getting the state and setting up the // watch type racyEndpoint struct { stateGot bool maxTime time.Time delta time.Duration lock sync.RWMutex } func (rep *racyEndpoint) GetProperty(prop string) (interface{}, error) { switch prop { case "state": rep.lock.Lock() defer rep.lock.Unlock() rep.stateGot = true rep.maxTime = time.Now().Add(rep.delta) return uint32(networkmanager.Connecting), nil case "PrimaryConnection": return dbus.ObjectPath("/something"), nil default: return nil, nil } } func (rep *racyEndpoint) WatchSignal(member string, f func(...interface{}), d func()) (bus.Cancellable, error) { if member == "StateChanged" { // we count never having gotten the state as happening "after" now. rep.lock.RLock() defer rep.lock.RUnlock() ok := !rep.stateGot || time.Now().Before(rep.maxTime) go func() { if ok { f(uint32(networkmanager.ConnectedGlobal)) } d() }() } return nil, nil } func (rep *racyEndpoint) WatchProperties(f func(map[string]dbus.Variant, []string), d func()) (bus.Cancellable, error) { return nil, nil } func (*racyEndpoint) Close() {} func (*racyEndpoint) Dial() error { return nil } func (*racyEndpoint) String() string { return "racyEndpoint" } func (*racyEndpoint) Call(string, []interface{}, ...interface{}) error { return nil } func (*racyEndpoint) GrabName(bool) <-chan error { return nil } func (*racyEndpoint) WatchMethod(bus.DispatchMap, string, ...interface{}) {} func (*racyEndpoint) Signal(member string, suffix string, args []interface{}) error { return nil } func (*racyEndpoint) SetProperty(string, string, interface{}) error { return nil } var _ bus.Endpoint = (*racyEndpoint)(nil) // takeNext takes a value from given channel with a 1s timeout func takeNext(ch <-chan networkmanager.State) networkmanager.State { select { case <-time.After(time.Second): panic("channel stuck: too long waiting") case v := <-ch: return v } } // test that if the nm state goes from connecting to connected very // shortly after calling GetState, we don't lose the event. func (s *ConnSuite) TestStartAvoidsRace(c *C) { for delta := time.Second; delta > 1; delta /= 2 { rep := &racyEndpoint{delta: delta} cs := ConnectedState{config: ConnectivityConfig{}, log: s.log, endp: rep} f := Commentf("when delta=%s", delta) c.Assert(cs.start(), Equals, networkmanager.Connecting, f) c.Assert(takeNext(cs.networkStateCh), Equals, networkmanager.ConnectedGlobal, f) } } /* tests for step() */ type testWebchk func(ch chan<- bool) func (x testWebchk) Webcheck(ch chan<- bool) { x(ch) } func (x testWebchk) Close() { } func (s *ConnSuite) TestSteps(c *C) { var webget_p condition.Interface = condition.Work(true) recheck_timeout := 50 * time.Millisecond cfg := ConnectivityConfig{ RecheckTimeout: config.ConfigTimeDuration{recheck_timeout}, } ch := make(chan networkmanager.State, 10) cs := &ConnectedState{ config: cfg, networkStateCh: ch, timer: time.NewTimer(time.Second), log: s.log, webchk: testWebchk(func(ch chan<- bool) { ch <- webget_p.OK() }), lastSent: false, } ch <- networkmanager.ConnectedGlobal f, e := cs.step() c.Check(e, IsNil) c.Check(f, Equals, true) ch <- networkmanager.Disconnected ch <- networkmanager.ConnectedGlobal f, e = cs.step() c.Check(e, IsNil) c.Check(f, Equals, false) f, e = cs.step() c.Check(e, IsNil) c.Check(f, Equals, true) // same scenario, but with failing web check webget_p = condition.Fail2Work(1) ch <- networkmanager.Disconnected ch <- networkmanager.ConnectedGlobal f, e = cs.step() c.Check(e, IsNil) c.Check(f, Equals, false) // first false is from the Disconnected // the next call to Step will time out _ch := make(chan bool, 1) _t := time.NewTimer(recheck_timeout / 2) go func() { f, e := cs.step() c.Check(e, IsNil) _ch <- f }() select { case <-_ch: c.Fatal("test failed to timeout") case <-_t.C: } // now an recheckTimeout later, we'll get true c.Check(<-_ch, Equals, true) ch <- networkmanager.Disconnected // this should trigger a 'false' ch <- networkmanager.Disconnected // this should not ch <- networkmanager.ConnectedGlobal // this should trigger a 'true' f, e = cs.step() c.Check(e, IsNil) c.Check(f, Equals, false) f, e = cs.step() c.Check(e, IsNil) c.Check(f, Equals, true) close(ch) // this should make it error out _, e = cs.step() c.Check(e, NotNil) } /* tests for ConnectedState() */ // yes, this is an integration test func (s *ConnSuite) TestRun(c *C) { ts := httptest.NewServer(mkHandler(staticText)) defer ts.Close() cfg := ConnectivityConfig{ ConnectivityCheckURL: ts.URL, ConnectivityCheckMD5: staticHash, RecheckTimeout: config.ConfigTimeDuration{time.Second}, } endp := testingbus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(networkmanager.Disconnected), helloCon, uint32(networkmanager.Disconnected), helloCon, ) watchTicker := make(chan []interface{}) testingbus.SetWatchSource(endp, "StateChanged", watchTicker) nopTicker := make(chan []interface{}) testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker) out := make(chan bool) dt := time.Second / 10 timer := time.NewTimer(dt) cs := New(endp, cfg, s.log) defer cs.Cancel() go cs.Track(out) var v bool expecteds := []struct { p bool s string todo string }{ {false, "first state is always false", ""}, {true, "then it should be true as per ConnectedGlobal above", "ConnectedGlobal"}, {false, "then it should be false (Disconnected)", "Disconnected"}, {false, "then it should be false again because it's restarted", "close"}, } defer func() { if watchTicker != nil { close(watchTicker) } }() defer close(nopTicker) for i, expected := range expecteds { switch expected.todo { case "ConnectedGlobal": watchTicker <- []interface{}{uint32(networkmanager.ConnectedGlobal)} case "Disconnected": watchTicker <- []interface{}{uint32(networkmanager.Disconnected)} case "close": close(watchTicker) watchTicker = nil } timer.Reset(dt) select { case v = <-out: break case <-timer.C: c.Fatalf("Timed out before getting value (#%d: %s)", i+1, expected.s) } c.Assert(v, Equals, expected.p, Commentf(expected.s)) } } func (s *ConnSuite) TestRun4Active(c *C) { ts := httptest.NewServer(mkHandler(staticText)) defer ts.Close() cfg := ConnectivityConfig{ ConnectivityCheckURL: ts.URL, ConnectivityCheckMD5: staticHash, RecheckTimeout: config.ConfigTimeDuration{time.Second}, } endp := testingbus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(networkmanager.ConnectedGlobal), helloCon, ) watchTicker := make(chan []interface{}) testingbus.SetWatchSource(endp, "PropertiesChanged", watchTicker) nopTicker := make(chan []interface{}) testingbus.SetWatchSource(endp, "StateChanged", nopTicker) out := make(chan bool) dt := time.Second / 10 timer := time.NewTimer(dt) cs := New(endp, cfg, s.log) defer cs.Cancel() go cs.Track(out) var v bool expecteds := []struct { p bool s string changedConn bool }{ {false, "first state is always false", false}, {true, "then it should be true as per ConnectedGlobal above", false}, {false, "then, false (PrimaryConnection changed)", true}, {true, "then it should be true (webcheck passed)", false}, } defer func() { if watchTicker != nil { close(watchTicker) } }() defer close(nopTicker) for i, expected := range expecteds { if expected.changedConn { watchTicker <- []interface{}{helloConProps} } timer.Reset(dt) select { case v = <-out: break case <-timer.C: c.Fatalf("Timed out before getting value (#%d: %s)", i+1, expected.s) } c.Assert(v, Equals, expected.p, Commentf(expected.s)) } } ubuntu-push-0.68+16.04.20160310.2/bus/haptic/0000755000015600001650000000000012670364532020514 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/haptic/haptic_test.go0000644000015600001650000001234312670364255023357 0ustar pbuserpbgroup00000000000000/* 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 haptic import ( "encoding/json" "testing" . "launchpad.net/gocheck" testibus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/click" clickhelp "launchpad.net/ubuntu-push/click/testing" "launchpad.net/ubuntu-push/launch_helper" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) func TestHaptic(t *testing.T) { TestingT(t) } type hapticSuite struct { log *helpers.TestLogger app *click.AppId acc *mockAccounts } type mockAccounts struct { vib bool sil bool snd string err error } func (m *mockAccounts) Start() error { return m.err } func (m *mockAccounts) Cancel() error { return m.err } func (m *mockAccounts) SilentMode() bool { return m.sil } func (m *mockAccounts) Vibrate() bool { return m.vib } func (m *mockAccounts) MessageSoundFile() string { return m.snd } func (m *mockAccounts) String() string { return "" } var _ = Suite(&hapticSuite{}) func (hs *hapticSuite) SetUpTest(c *C) { hs.log = helpers.NewTestLogger(c, "debug") hs.app = clickhelp.MustParseAppId("com.example.test_test-app_0") hs.acc = &mockAccounts{true, false, "xyzzy", nil} } // checks that Present() actually calls VibratePattern func (hs *hapticSuite) TestPresentPresents(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) ec := New(endp, hs.log, hs.acc, nil) notif := launch_helper.Notification{RawVibration: json.RawMessage(`{"pattern": [200, 100], "repeat": 2}`)} c.Check(ec.Present(hs.app, "nid", ¬if), Equals, true) callArgs := testibus.GetCallArgs(endp) c.Assert(callArgs, HasLen, 1) c.Check(callArgs[0].Member, Equals, "VibratePattern") c.Check(callArgs[0].Args, DeepEquals, []interface{}{[]uint32{200, 100}, uint32(2)}) } // check that Present() defaults Repeat to 1 func (hs *hapticSuite) TestPresentDefaultsRepeatTo1(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) ec := New(endp, hs.log, hs.acc, nil) // note: no Repeat: notif := launch_helper.Notification{RawVibration: json.RawMessage(`{"pattern": [200, 100]}`)} c.Check(ec.Present(hs.app, "nid", ¬if), Equals, true) callArgs := testibus.GetCallArgs(endp) c.Assert(callArgs, HasLen, 1) c.Check(callArgs[0].Member, Equals, "VibratePattern") // note: Repeat of 1: c.Check(callArgs[0].Args, DeepEquals, []interface{}{[]uint32{200, 100}, uint32(1)}) } // check that Present() doesn't call VibratePattern if things are not right func (hs *hapticSuite) TestSkipIfMissing(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) ec := New(endp, hs.log, hs.acc, nil) // no Vibration in the notificaton c.Check(ec.Present(hs.app, "", &launch_helper.Notification{}), Equals, false) // empty Vibration c.Check(ec.Present(hs.app, "", &launch_helper.Notification{RawVibration: nil}), Equals, false) // empty empty vibration c.Check(ec.Present(hs.app, "", &launch_helper.Notification{RawVibration: json.RawMessage(`{}`)}), Equals, false) } // check that Present() does not present if the accounts' Vibrate() returns false func (hs *hapticSuite) TestPresentSkipsIfVibrateDisabled(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) fallback := &launch_helper.Vibration{Pattern: []uint32{200, 100}, Repeat: 2} ec := New(endp, hs.log, hs.acc, fallback) notif := launch_helper.Notification{RawVibration: json.RawMessage(`true`)} c.Assert(ec.Present(hs.app, "nid", ¬if), Equals, true) // ok! hs.acc.vib = false c.Check(ec.Present(hs.app, "nid", ¬if), Equals, false) } // check that Present() panics if the notification is nil func (hs *hapticSuite) TestPanicsIfNil(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) ec := New(endp, hs.log, hs.acc, nil) // no notification at all c.Check(func() { ec.Present(hs.app, "", nil) }, Panics, `please check notification is not nil before calling present`) } // check that Present() uses the fallback if appropriate func (hs *hapticSuite) TestPresentPresentsFallback(c *C) { endp := testibus.NewTestingEndpoint(nil, condition.Work(true)) fallback := &launch_helper.Vibration{Pattern: []uint32{200, 100}, Repeat: 2} ec := New(endp, hs.log, hs.acc, fallback) notif := launch_helper.Notification{RawVibration: json.RawMessage(`false`)} c.Check(ec.Present(hs.app, "nid", ¬if), Equals, false) notif = launch_helper.Notification{RawVibration: json.RawMessage(`true`)} c.Check(ec.Present(hs.app, "nid", ¬if), Equals, true) callArgs := testibus.GetCallArgs(endp) c.Assert(callArgs, HasLen, 1) c.Check(callArgs[0].Member, Equals, "VibratePattern") c.Check(callArgs[0].Args, DeepEquals, []interface{}{[]uint32{200, 100}, uint32(2)}) } ubuntu-push-0.68+16.04.20160310.2/bus/haptic/haptic.go0000644000015600001650000000506112670364255022317 0ustar pbuserpbgroup00000000000000/* 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 haptic can present notifications as a vibration pattern // using the usensord/haptic interface package haptic import ( "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/bus/accounts" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/logger" ) // usensord/haptic lives on a well-knwon bus.Address var BusAddress bus.Address = bus.Address{ Interface: "com.canonical.usensord.haptic", Path: "/com/canonical/usensord/haptic", Name: "com.canonical.usensord", } // Haptic encapsulates info needed to call out to usensord/haptic type Haptic struct { bus bus.Endpoint log logger.Logger acc accounts.Accounts fallback *launch_helper.Vibration } // New returns a new Haptic that'll use the provided bus.Endpoint func New(endp bus.Endpoint, log logger.Logger, acc accounts.Accounts, fallback *launch_helper.Vibration) *Haptic { return &Haptic{endp, log, acc, fallback} } // Present presents the notification via a vibrate pattern func (haptic *Haptic) Present(_ *click.AppId, nid string, notification *launch_helper.Notification) bool { if notification == nil { panic("please check notification is not nil before calling present") } if !haptic.acc.Vibrate() { haptic.log.Debugf("[%s] vibrate disabled by user.", nid) return false } vib := notification.Vibration(haptic.fallback) if vib == nil { haptic.log.Debugf("[%s] notification has no Vibrate.", nid) return false } pattern := vib.Pattern repeat := vib.Repeat if repeat == 0 { repeat = 1 } if len(pattern) == 0 { haptic.log.Debugf("[%s] not enough information in the Vibrate to create a pattern", nid) return false } haptic.log.Debugf("[%s] vibrating %d times to the tune of %v", nid, repeat, pattern) err := haptic.bus.Call("VibratePattern", bus.Args(pattern, repeat)) if err != nil { haptic.log.Errorf("[%s] call to VibratePattern returned %v", nid, err) return false } return true } ubuntu-push-0.68+16.04.20160310.2/bus/urfkill/0000755000015600001650000000000012670364532020714 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/bus/urfkill/urfkill.go0000644000015600001650000001073612670364255022724 0ustar pbuserpbgroup00000000000000/* Copyright 2015 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 urfkill wraps a couple of URfkill's DBus API points to // watch for flight mode state changes. package urfkill import ( "launchpad.net/go-dbus/v1" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/logger" ) // URfkill lives on a well-knwon bus.Address var BusAddress bus.Address = bus.Address{ Interface: "org.freedesktop.URfkill", Path: "/org/freedesktop/URfkill", Name: "org.freedesktop.URfkill", } // URfkill lives on a well-knwon bus.Address var WLANKillswitchBusAddress bus.Address = bus.Address{ Interface: "org.freedesktop.URfkill.Killswitch", Path: "/org/freedesktop/URfkill/WLAN", Name: "org.freedesktop.URfkill", } /***************************************************************** * URfkill (and its implementation) */ type KillswitchState int32 const ( KillswitchStateUnblocked KillswitchState = 0 KillswitchStateSoftBlocked KillswitchState = 1 KillswitchStateHardBlocked KillswitchState = 2 ) type URfkill interface { // IsFlightMode returns flight mode state. IsFlightMode() bool // WatchFlightMode listens for changes to URfkill's flight // mode state, and sends them out over the channel returned. WatchFlightMode() (<-chan bool, bus.Cancellable, error) // GetWLANKillswitchState fetches and returns URfkill's // WLAN killswitch state. GetWLANKillswitchState() KillswitchState // WatchWLANKillswitchState listens for changes of URfkill's // WLAN killswtich state, and sends them out over the channel returned. WatchWLANKillswitchState() (<-chan KillswitchState, bus.Cancellable, error) } type uRfkill struct { bus bus.Endpoint wlanKillswitch bus.Endpoint log logger.Logger } // New returns a new URfkill that'll use the provided bus.Endpoints // for BusAddress and WLANKillswitchBusAddress func New(endp bus.Endpoint, wlanKillswitch bus.Endpoint, log logger.Logger) URfkill { return &uRfkill{endp, wlanKillswitch, log} } // ensure uRfkill implements URfkill var _ URfkill = &uRfkill{} /* public methods */ func (ur *uRfkill) IsFlightMode() bool { var res bool err := ur.bus.Call("IsFlightMode", bus.Args(), &res) if err != nil { ur.log.Errorf("failed getting flight-mode state: %s", err) ur.log.Debugf("defaulting flight-mode state to false") return false } return res } func (ur *uRfkill) WatchFlightMode() (<-chan bool, bus.Cancellable, error) { ch := make(chan bool) w, err := ur.bus.WatchSignal("FlightModeChanged", func(ns ...interface{}) { stbool, ok := ns[0].(bool) if !ok { ur.log.Errorf("got weird flight-mode state: %#v", ns[0]) return } ur.log.Debugf("got flight-mode change: %v", stbool) ch <- stbool }, func() { close(ch) }) if err != nil { ur.log.Debugf("Failed to set up the watch: %s", err) return nil, nil, err } return ch, w, nil } func (ur *uRfkill) GetWLANKillswitchState() KillswitchState { got, err := ur.wlanKillswitch.GetProperty("state") if err != nil { ur.log.Errorf("failed getting WLANKillswitchState: %s", err) ur.log.Debugf("defaulting WLANKillswitchState to true") return KillswitchStateUnblocked } v, ok := got.(int32) if !ok { ur.log.Errorf("got weird WLANKillswitchState: %#v", got) return KillswitchStateUnblocked } return KillswitchState(v) } func (ur *uRfkill) WatchWLANKillswitchState() (<-chan KillswitchState, bus.Cancellable, error) { ch := make(chan KillswitchState) w, err := ur.wlanKillswitch.WatchProperties( func(changed map[string]dbus.Variant, invalidated []string) { v, ok := changed["state"] if !ok { return } st, ok := v.Value.(int32) if !ok { ur.log.Errorf("got weird WLANKillswitchState via PropertiesChanged: %#v", v) return } ur.log.Debugf("got WLANKillswitchState change: %v", st) ch <- KillswitchState(st) }, func() { close(ch) }) if err != nil { ur.log.Debugf("failed to set up the watch: %s", err) return nil, nil, err } return ch, w, nil } ubuntu-push-0.68+16.04.20160310.2/bus/urfkill/urfkill_test.go0000644000015600001650000001730712670364255023764 0ustar pbuserpbgroup00000000000000/* Copyright 2015 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 urfkill import ( "testing" "launchpad.net/go-dbus/v1" . "launchpad.net/gocheck" testingbus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/logger" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) // hook up gocheck func Test(t *testing.T) { TestingT(t) } type URSuite struct { log logger.Logger } var _ = Suite(&URSuite{}) func (s *URSuite) SetUpTest(c *C) { s.log = helpers.NewTestLogger(c, "debug") } func (s *URSuite) TestNew(c *C) { ur := New(testingbus.NewTestingEndpoint(nil, condition.Work(true)), nil, s.log) c.Check(ur, NotNil) } // IsFlightMode returns the right state when everything works func (s *URSuite) TestIsFlightMode(c *C) { endp := testingbus.NewTestingEndpoint(nil, condition.Work(true), true) ur := New(endp, nil, s.log) state := ur.IsFlightMode() c.Check(state, Equals, true) callArgs := testingbus.GetCallArgs(endp) c.Assert(callArgs, HasLen, 1) c.Assert(callArgs[0].Member, Equals, "IsFlightMode") c.Assert(callArgs[0].Args, HasLen, 0) } // IsFlightMode returns the right state when dbus fails func (s *URSuite) TestIsFlightModeFail(c *C) { ur := New(testingbus.NewTestingEndpoint(nil, condition.Work(false)), nil, s.log) state := ur.IsFlightMode() c.Check(state, Equals, false) } // IsFlightMode returns the right state when dbus works but delivers // rubbish values func (s *URSuite) TestIsFlightModeRubbishValues(c *C) { ur := New(testingbus.NewTestingEndpoint(nil, condition.Work(true), "broken"), nil, s.log) state := ur.IsFlightMode() c.Check(state, Equals, false) } // IsFlightMode returns the right state when dbus works but delivers a rubbish structure func (s *URSuite) TestIsFlightModeRubbishStructure(c *C) { ur := New(testingbus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{}), nil, s.log) state := ur.IsFlightMode() c.Check(state, Equals, false) } // WatchFightMode sends a stream of states over the channel func (s *URSuite) TestWatchFlightMode(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), false, true, false) ur := New(tc, nil, s.log) ch, w, err := ur.WatchFlightMode() c.Assert(err, IsNil) defer w.Cancel() l := []bool{<-ch, <-ch, <-ch} c.Check(l, DeepEquals, []bool{false, true, false}) } // WatchFlightMode returns on error if the dbus call fails func (s *URSuite) TestWatchFlightModeFails(c *C) { ur := New(testingbus.NewTestingEndpoint(nil, condition.Work(false)), nil, s.log) _, _, err := ur.WatchFlightMode() c.Check(err, NotNil) } // WatchFlightMode calls close on its channel when the watch bails func (s *URSuite) TestWatchFlightModeClosesOnWatchBail(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true)) ur := New(tc, nil, s.log) ch, w, err := ur.WatchFlightMode() c.Assert(err, IsNil) defer w.Cancel() _, ok := <-ch c.Check(ok, Equals, false) } // WatchFlightMode survives rubbish values func (s *URSuite) TestWatchFlightModeSurvivesRubbishValues(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true), "gorp") ur := New(tc, nil, s.log) ch, w, err := ur.WatchFlightMode() c.Assert(err, IsNil) defer w.Cancel() _, ok := <-ch c.Check(ok, Equals, false) } // GetWLANKillState returns the right state when everything works func (s *URSuite) TestGetWLANKillState(c *C) { ur := New(nil, testingbus.NewTestingEndpoint(nil, condition.Work(true), KillswitchStateSoftBlocked), s.log) st := ur.GetWLANKillswitchState() c.Check(st, Equals, KillswitchStateSoftBlocked) } // GetWLANKillswitchState returns the right state when dbus fails func (s *URSuite) TestGetWLANKillswitchStateFail(c *C) { ur := New(nil, testingbus.NewTestingEndpoint(nil, condition.Work(false)), s.log) st := ur.GetWLANKillswitchState() c.Check(st, Equals, KillswitchStateUnblocked) } // GetWLANKillswitchState returns the right state when dbus works but delivers rubbish values func (s *URSuite) TestGetWLANKillswitchStateRubbishValues(c *C) { ur := New(nil, testingbus.NewTestingEndpoint(nil, condition.Work(true), "broken"), s.log) st := ur.GetWLANKillswitchState() c.Check(st, Equals, KillswitchStateUnblocked) } // GetWLANKillswitchState returns the right state when dbus works but delivers a rubbish structure func (s *URSuite) TestGetWLANKillswitchStateRubbishStructure(c *C) { ur := New(nil, testingbus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{}), s.log) st := ur.GetWLANKillswitchState() c.Check(st, Equals, KillswitchStateUnblocked) } func mkWLANKillswitchStateMap(st KillswitchState) map[string]dbus.Variant { m := make(map[string]dbus.Variant) m["state"] = dbus.Variant{int32(st)} return m } // WatchWLANKillswitchState sends a stream of WLAN killswitch states over the channel func (s *URSuite) TestWatchWLANKillswitchState(c *C) { tc := testingbus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{mkWLANKillswitchStateMap(KillswitchStateUnblocked), []string{}}, []interface{}{mkWLANKillswitchStateMap(KillswitchStateHardBlocked), []string{}}, []interface{}{mkWLANKillswitchStateMap(KillswitchStateUnblocked), []string{}}, ) ur := New(nil, tc, s.log) ch, w, err := ur.WatchWLANKillswitchState() c.Assert(err, IsNil) defer w.Cancel() l := []KillswitchState{<-ch, <-ch, <-ch} c.Check(l, DeepEquals, []KillswitchState{KillswitchStateUnblocked, KillswitchStateHardBlocked, KillswitchStateUnblocked}) } // WatchWLANKillswitchState returns on error if the dbus call fails func (s *URSuite) TestWatchWLANKillswitchStateFails(c *C) { ur := New(nil, testingbus.NewTestingEndpoint(nil, condition.Work(false)), s.log) _, _, err := ur.WatchWLANKillswitchState() c.Check(err, NotNil) } // WatchWLANKillswitchState calls close on its channel when the watch bails func (s *URSuite) TestWatchWLANKillswitchStateClosesOnWatchBail(c *C) { tc := testingbus.NewTestingEndpoint(nil, condition.Work(true)) ur := New(nil, tc, s.log) ch, w, err := ur.WatchWLANKillswitchState() c.Assert(err, IsNil) defer w.Cancel() _, ok := <-ch c.Check(ok, Equals, false) } // WatchWLANKillswitchState ignores non-WLAN-killswitch PropertiesChanged func (s *URSuite) TestWatchWLANKillswitchStateIgnoresIrrelevant(c *C) { tc := testingbus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{map[string]dbus.Variant{"foo": dbus.Variant{}}, []string{}}, []interface{}{mkWLANKillswitchStateMap(KillswitchStateUnblocked), []string{}}, ) ur := New(nil, tc, s.log) ch, w, err := ur.WatchWLANKillswitchState() c.Assert(err, IsNil) defer w.Cancel() v, ok := <-ch c.Check(ok, Equals, true) c.Check(v, Equals, KillswitchStateUnblocked) } // WatchWLANKillswitchState ignores rubbish WLAN killswitch state func (s *URSuite) TestWatchWLANKillswitchStateIgnoresRubbishValues(c *C) { tc := testingbus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), []interface{}{map[string]dbus.Variant{"state": dbus.Variant{-12}}, []string{}}, []interface{}{mkWLANKillswitchStateMap(KillswitchStateSoftBlocked), []string{}}, ) ur := New(nil, tc, s.log) ch, w, err := ur.WatchWLANKillswitchState() c.Assert(err, IsNil) defer w.Cancel() v, ok := <-ch c.Check(ok, Equals, true) c.Check(v, Equals, KillswitchStateSoftBlocked) } ubuntu-push-0.68+16.04.20160310.2/tests/0000755000015600001650000000000012670364532017615 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/0000755000015600001650000000000012670364532021635 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/run.sh0000755000015600001650000000135712670364255023010 0ustar pbuserpbgroup00000000000000#!/usr/bin/env bash set -e set -u usage() { cat << EOF usage: $0 options OPTIONS: -t fqn of the tests to run (module, class or method) -d adb device_id -v run tests in verbose mode EOF } while getopts "t:d:v" opt; do case $opt in d) DEVICE_ID=$OPTARG;; v) VERBOSITY="-v";; t) TESTS=$OPTARG;; *) usage exit 1 ;; esac done VERBOSITY=${VERBOSITY:-""} TESTS=${TESTS:-"push_notifications"} DEVICE_ID=${DEVICE_ID:-"emulator-5554"} BASE_DIR="ubuntu-push/src/launchpad.net" BRANCH_DIR="$BASE_DIR/ubuntu-push" adb -s ${DEVICE_ID} shell "su - phablet bash -c 'cd ${BRANCH_DIR}/tests/autopilot/; /sbin/initctl stop unity8; autopilot3 run ${VERBOSITY} ${TESTS}'" ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/setup.sh0000755000015600001650000000543612670364255023346 0ustar pbuserpbgroup00000000000000#!/usr/bin/env bash set -e set -u usage() { cat << EOF usage: $0 -H [-b ] OPTIONS: -H host/IP where the push server is running. -b tests branch url -d adb device id -u run apt-get update in the device before installing dependencies EOF } while getopts "H:b:d:u" opt; do case $opt in H) PUSH_SERVER=$OPTARG;; b) BRANCH_URL=$OPTARG;; d) DEVICE_ID=$OPTARG;; u) APT_UPDATE="1";; *) usage exit 1 ;; esac done if [[ -z ${PUSH_SERVER} ]] then usage exit 1 fi DEVICE_ID=${DEVICE_ID:-"emulator-5554"} BRANCH_URL=${BRANCH_URL:-"lp:ubuntu-push/automatic"} ROOT_DIR=`bzr root` APT_UPDATE=${APT_UPDATE:-"0"} DEPS_OK=$(adb -s ${DEVICE_ID} shell "[ ! -f autopilot-deps.ok ] && echo 1 || echo 0") # get substring [0] of the returned 1/0 value because we get a trailing ^M if [[ "${DEPS_OK:0:1}" == "1" ]] then echo "installing dependencies" if [[ "${APT_UPDATE}" == "1" ]] then # in case apt fails to fetch some packages adb -s ${DEVICE_ID} shell "DEBIAN_FRONTEND=noninteractive apt-get -y update" fi # required for running the autopilot tests adb -s ${DEVICE_ID} shell "DEBIAN_FRONTEND=noninteractive apt-get -y install unity8-autopilot unity-scope-click bzr" adb -s ${DEVICE_ID} shell "touch autopilot-deps.ok" fi # fetch the code BASE_DIR="/home/phablet/ubuntu-push/src/launchpad.net" BRANCH_DIR="$BASE_DIR/ubuntu-push" BRANCH_OK=$(adb -s ${DEVICE_ID} shell "su - phablet bash -c '[ ! -d "${BRANCH_DIR}/tests/autopilot" ] && echo 1 || echo 0'") if [[ "${BRANCH_OK:0:1}" == 1 ]] || [[ "${BRANCH_URL}" != "lp:ubuntu-push/automatic" ]] then adb -s ${DEVICE_ID} shell "su - phablet bash -c 'rm -Rf ${BRANCH_DIR}'" echo "fetching code." adb -s ${DEVICE_ID} shell "su - phablet bash -c 'mkdir -p ${BASE_DIR}'" adb -s ${DEVICE_ID} shell "su - phablet bash -c 'bzr branch ${BRANCH_URL} ${BRANCH_DIR}'" fi adb -s ${DEVICE_ID} shell "su - phablet bash -c \"sed -i 's/addr =.*/addr = ${PUSH_SERVER}/' ${BRANCH_DIR}/tests/autopilot/push_notifications/config/push.conf\"" # copy the trivial-helper.sh as the heper for the messaging-app (used in the tests) HELPER_OK=$(adb -s ${DEVICE_ID} shell "[ ! -f /usr/lib/ubuntu-push-client/legacy-helpers/messaging-app ] && echo 1 || echo 0") if [[ "${HELPER_OK:0:1}" == 1 ]] then adb -s ${DEVICE_ID} shell "cp ${BRANCH_DIR}/scripts/trivial-helper.sh /usr/lib/ubuntu-push-client/legacy-helpers/messaging-app" fi # change the local/dev server config, listen in all interfaces sed -i 's/127.0.0.1/0.0.0.0/g' ${ROOT_DIR}/sampleconfigs/dev.json # and start it cd ${ROOT_DIR}; make run-server-dev # remove the trivial helper for the messaging-app adb -s ${DEVICE_ID} shell "rm /usr/lib/ubuntu-push-client/legacy-helpers/messaging-app" ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/0000755000015600001650000000000012670364532025545 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/__init__.py0000644000015600001650000000147712670364255027671 0ustar pbuserpbgroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Push Notifications Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . # """push-notifications autopilot tests.""" ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/tests/0000755000015600001650000000000012670364532026707 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/tests/__init__.py0000644000015600001650000003405612670364255031032 0ustar pbuserpbgroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Push Notifications Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . # """push-notifications autopilot tests.""" import copy import datetime import time import evdev from autopilot.introspection import dbus from autopilot.exceptions import StateNotFoundError from push_notifications import config as push_config import push_notifications.helpers.push_notifications_helper as push_helper from testtools.matchers import Equals from unity8.shell.tests import UnityTestCase class PushNotificationTestBase(UnityTestCase): """ Base class for push notification test cases """ @classmethod def setUpClass(cls): """ Executed once before all the tests run Restart the push client using the test config """ test_config = push_helper.PushClientConfig.read_config( push_config.get_config_file()) push_client_controller = push_helper.PushClientController() push_client_controller.restart_push_client_using_config(test_config) @classmethod def tearDownClass(cls): """ Executed once after all tests have completed Reset the push client to use the device's original config """ push_client_controller = push_helper.PushClientController() push_client_controller.restart_push_client_using_config(None) def setUp(self): """ Setup phase executed before each test """ # setup super(PushNotificationTestBase, self).setUp() # read and store the test config data self.test_config = push_helper.PushClientConfig.read_config( push_config.get_config_file()) # create a push helper object which will do all the message sending self.push_helper = push_helper.PushNotificationHelper() # create a push controller object self.push_client_controller = push_helper.PushClientController() # get and store device and build info self.device_info = self.push_helper.get_device_info() # start unity8 self._qml_mock_enabled = False self._data_dirs_mock_enabled = False self.unity = self.launch_unity() # dismiss any outstanding dialog self.dismiss_outstanding_dialog() def create_device_info_copy(self): """ Return a copy of the device's model and build info :return: DeviceNotificationData object containging device's model and build info """ return copy.deepcopy(self.device_info) def press_power_button(self): """ Simulate a power key press event """ uinput = evdev.UInput(name='push-autopilot-power-button', devnode='/dev/autopilot-uinput') # One press and release to turn screen off (locking unity) uinput.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_POWER, 1) uinput.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_POWER, 0) uinput.syn() def unlock_greeter(self): """ Unlock the greeter to display home screen """ self.main_window.get_greeter().swipe() def validate_response(self, response, expected_status_code=200): """ Validate the received response status code against expected code :param response: response to validate :param expected_status_code: value of expected http status code """ self.assertThat(response.status, Equals(expected_status_code)) def assert_notification_dialog(self, props, summary=None, body=None, icon=True, secondary_icon=True, opacity=None): """ Assert that the properties of the notification are as expected :param props: properties of the notification object :param summary: expected notification summary value :param body: expected notification body value :param icon: expected icon status :param secondary_icon: expected secondary icon status :param opacity: expected opacity value """ if summary is not None: self.assertEqual(props['summary'], summary) if body is not None: self.assertEqual(props['body'], body) if opacity is not None: self.assertEqual(props['opacity'], opacity) if icon: self.assertNotEqual(props['iconSource'], '') else: self.assertEqual(props['iconSource'], '') if secondary_icon: self.assertNotEqual(props['secondaryIconSource'], '') else: self.assertEqual(props['secondaryIconSource'], '') def validate_notification_not_displayed(self, wait=True): """ Validate that the notification is not displayed If wait is True then wait for default timeout period If wait is False then do not wait at all :param wait: wait status """ found = True try: if wait is True: self.main_window.wait_select_single( 'Notification', objectName='notification1') else: self.main_window.select_single( 'Notification', objectName='notification1') except dbus.StateNotFoundError: found = False self.assertFalse(found) def send_push_broadcast_message(self): """ Send a push broadcast message which should trigger a notification to be displayed on the client """ # create a copy of the device's build info device_info = self.create_device_info_copy() # increment the build number to trigger an update device_info.inc_build_number() # create push message based on the device data push_msg = self.push_helper.create_push_message( data=device_info.to_json()) # send the notification message to the server and check response response = self.push_helper.send_push_broadcast_notification( push_msg.to_json(), self.test_config.server_listener_addr) self.validate_response(response) def _get_notification_obj_and_props(self, kind, name): """Return the dialog and it's properties. This is an alternative to wait_select_single, with a faster loop and properties retrieval as it seems that popup/dialogs, timing and default wait_select_single aren't a good match. """ max_retry = 10 for i in range(10): try: dialog = self.main_window.select_single( 'Notification', objectName='notification1') props = dialog.get_properties() except Exception: time.sleep(0.1) if i+1 == max_retry: raise else: return (dialog, props) def get_notification_dialog(self, wait=True): """ Get the notification dialog and properties being displayed on screen If wait is True then wait for default timeout period If wait is False then do not wait at all :param wait: wait status :return: dialog introspection object """ if wait is True: return self._get_notification_obj_and_props('Notification', 'notification1') else: dialog = self.main_window.select_single( 'Notification', objectName='notification1') return dialog, dialog.get_properties() def validate_and_dismiss_notification_dialog(self, message, secondary_icon=True): """ Validate a notification dialog is displayed and dismiss it :param message: expected message displayed in summary """ # get the dialog dialog, props = self.get_notification_dialog() # validate dialog self.assert_notification_dialog( props, summary=message, secondary_icon=secondary_icon) # wait for dialog to dismiss automatically self.wait_until_dialog_dismissed(dialog) # check the dialog is no longer displayed self.validate_notification_not_displayed(wait=False) def validate_and_dismiss_broadcast_notification_dialog(self, message): """ Validate a broadcast notification dialog is displayed and dismiss it :param message: expected message displayed in summary """ self.validate_and_dismiss_notification_dialog(message, secondary_icon=False) def validate_and_tap_notification_dialog(self, message, secondary_icon=True): """ Validate a notification dialog is displayed and dismiss it :param message: expected message displayed in summary """ # get the dialog dialog, props = self.get_notification_dialog() # validate dialog self.assert_notification_dialog( props, summary=message, secondary_icon=secondary_icon) # tap the dialog self.touch.tap_object(dialog) # check the dialog is no longer displayed self.validate_notification_not_displayed(wait=False) def wait_until_dialog_dismissed(self, dialog): """Wait for the dialog to dismiss automatically""" dialog_disappeared = False try: # waiting for this property to change will cause a not found # exception once the dialog disappears dialog.visible.wait_for(False) except dbus.StateNotFoundError: dialog_disappeared = True # check that the dialog did disappear self.assertTrue(dialog_disappeared) def press_notification_dialog(self, dialog): """ Press the dialog to dismiss it """ self.touch.tap_object(dialog) def dismiss_outstanding_dialog(self): """ Dismiss outstanding notification dialog that may be displayed from an aborted previous test """ try: dialog = self.main_window.select_single( 'Notification', objectName='notification1') except dbus.StateNotFoundError: dialog = None if dialog: self.wait_until_dialog_dismissed(dialog) # unicast messages def send_unicast_notification(self, icon="", body="A unicast message", summary="Look!", persist=False, popup=True, actions=[], emblem_counter={}): """Build and send a push unicast message. Which should trigger a notification to be displayed on the client """ # XXX: build a unicast message notif = {"notification": {"card": {"icon": icon, "summary": summary, "body": body, "popup": popup, "persist": persist, "actions": actions, }, "emblem-counter": emblem_counter, } } expire_on = datetime.datetime.utcnow() + datetime.timedelta(seconds=20) data = {'token': self.token, 'data': notif, 'appid': self.appid, 'expire_on': expire_on.replace(microsecond=0).isoformat()+"Z"} # send the notification message to the server and check response response = self.push_helper.send_unicast( data, self.test_config.server_listener_addr) self.validate_response(response) def get_messaging_menu(self): """Swiping open an indicator must show its correct title. See https://bugs.launchpad.net/ubuntu-ux/+bug/1253804 . """ indicator_page = self.main_window.open_indicator_page( "indicator-messages") return indicator_page def validate_mmu_notification(self, body_text, title_text): # get the mmu notification and check the body and title. # swipe down and show the incomming page messaging = self.get_messaging_menu() # get the notification and check the body and title. menuItem0 = messaging.select_single('QQuickLoader', objectName='menuItem0') hmh = menuItem0.select_single('HeroMessageHeader') body = hmh.select_single("Label", objectName='body') self.assertEqual(body.text, body_text) title = hmh.select_single("Label", objectName='title') self.assertEqual(title.text, title_text) self.clear_mmu(ignore_missing=False) def clear_mmu(self, ignore_missing=True): # get the mmu notification and check the body and title. messaging = self.get_messaging_menu() # clear all notifications try: clear_all = messaging.select_single( 'ButtonMenu', objectName='indicator.remove-all') except StateNotFoundError: if not ignore_missing: raise return emptyLabel = messaging.select_single('Label', objectName='emptyLabel') self.assertFalse(emptyLabel.visible) self.touch.tap_object(clear_all) emptyLabel = messaging.select_single('Label', objectName='emptyLabel') self.assertTrue(emptyLabel.visible) ././@LongLink0000000000000000000000000000015300000000000011214 Lustar 00000000000000ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/tests/test_broadcast_notifications.pyubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/tests/test_broadcast_notificati0000644000015600001650000001672312670364255034057 0ustar pbuserpbgroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Push Notifications Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . # """Tests broadcast push notifications sent to the client""" import time from autopilot import platform from testtools import skipIf from push_notifications.tests import PushNotificationTestBase DEFAULT_DISPLAY_MESSAGE = "There's an updated system image." MMU_BODY = "Tap to open the system updater." MMU_TITLE = DEFAULT_DISPLAY_MESSAGE class TestPushClientBroadcast(PushNotificationTestBase): """Test cases for broadcast push notifications""" def test_broadcast_push_notification_screen_off(self): """ Send a push message whilst the device's screen is turned off Notification should still be displayed when it is turned on """ # Assumes greeter starts in locked state # Turn display off self.press_power_button() # send message self.send_push_broadcast_message() # wait before turning screen on time.sleep(2) # Turn display on self.press_power_button() # Assumes greeter starts in locked state self.unlock_greeter() # check the bubble is there self.validate_and_dismiss_notification_dialog(DEFAULT_DISPLAY_MESSAGE) # clear the mmu self.clear_mmu() def test_broadcast_push_notification_locked_greeter(self): """ Positive test case to send a valid broadcast push notification to the client and validate that a notification message is displayed whist the greeter screen is displayed """ self.send_push_broadcast_message() # check the bubble is there self.validate_and_dismiss_notification_dialog(DEFAULT_DISPLAY_MESSAGE) # Assumes greeter starts in locked state self.unlock_greeter() # clear the mmu self.clear_mmu() def test_broadcast_push_notification(self): """ Positive test case to send a valid broadcast push notification to the client and validate that a notification message is displayed """ # Assumes greeter starts in locked state self.unlock_greeter() self.send_push_broadcast_message() # check the bubble is there self.validate_and_dismiss_notification_dialog(DEFAULT_DISPLAY_MESSAGE) # clear the mmu self.clear_mmu() @skipIf(platform.model() == 'X86 Emulator', "Test not working in the emulator") def test_broadcast_push_notification_is_persistent(self): """ Positive test case to send a valid broadcast push notification to the client and validate that a notification message is displayed and includes a persitent mmu notification. """ # Assumes greeter starts in locked state self.unlock_greeter() self.send_push_broadcast_message() # check the bubble is there self.validate_and_dismiss_notification_dialog(DEFAULT_DISPLAY_MESSAGE) # check the mmu notification is there self.validate_mmu_notification(MMU_BODY, MMU_TITLE) def test_broadcast_push_notification_click_bubble_clears_mmu(self): """ Positive test case to send a valid broadcast push notification to the client and validate that a notification message is displayed """ # Assumes greeter starts in locked state self.unlock_greeter() self.send_push_broadcast_message() # check the bubble is there self.validate_and_tap_notification_dialog(DEFAULT_DISPLAY_MESSAGE) # swipe down and show the incomming page messaging = self.get_messaging_menu() emptyLabel = messaging.select_single('Label', objectName='emptyLabel') self.assertTrue(emptyLabel.visible) def test_broadcast_push_notification_on_connect(self): """ Send a broadcast notification whilst the push client is disconnected from the server. Then reconnect and ensure message is displayed """ # Assumes greeter starts in locked state self.unlock_greeter() self.push_client_controller.stop_push_client() self.send_push_broadcast_message() self.push_client_controller.start_push_client() # check the bubble is there self.validate_and_dismiss_notification_dialog(DEFAULT_DISPLAY_MESSAGE) # clear the mmu self.clear_mmu() def test_expired_broadcast_push_notification(self): """ Send an expired broadcast notification message to server """ # Assumes greeter starts in locked state self.unlock_greeter() # create notification message using past expiry time device_info = self.create_device_info_copy() device_info.inc_build_number() push_msg = self.push_helper.create_push_message( data=device_info.to_json(), expire_after=self.push_helper.get_past_iso_time()) # send message response = self.push_helper.send_push_broadcast_notification( push_msg.to_json(), self.test_config.server_listener_addr) # 400 status is received for an expired message self.validate_response(response, expected_status_code=400) # validate no notification is displayed self.validate_notification_not_displayed() def test_older_version_broadcast_push_notification(self): """ Send an old version broadcast notification message to server """ # Assumes greeter starts in locked state self.unlock_greeter() # create notification message using previous build number device_info = self.create_device_info_copy() device_info.dec_build_number() push_msg = self.push_helper.create_push_message( data=device_info.to_json()) response = self.push_helper.send_push_broadcast_notification( push_msg.to_json(), self.test_config.server_listener_addr) self.validate_response(response) # validate no notification is displayed self.validate_notification_not_displayed() def test_equal_version_broadcast_push_notification(self): """ Send an equal version broadcast notification message to server """ # Assumes greeter starts in locked state self.unlock_greeter() # create notification message using equal build number device_info = self.create_device_info_copy() push_msg = self.push_helper.create_push_message( data=device_info.to_json()) response = self.push_helper.send_push_broadcast_notification( push_msg.to_json(), self.test_config.server_listener_addr) self.validate_response(response) # validate no notification is displayed self.validate_notification_not_displayed() ././@LongLink0000000000000000000000000000015100000000000011212 Lustar 00000000000000ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/tests/test_unicast_notifications.pyubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/tests/test_unicast_notification0000644000015600001650000001756312670364255034123 0ustar pbuserpbgroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Push Notifications Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . # """Tests unicast push notifications sent to the client""" import os from autopilot import platform from testtools import skipIf from push_notifications.tests import PushNotificationTestBase class TestPushClientUnicast(PushNotificationTestBase): """Test cases for unicast push notifications.""" DEFAULT_DISPLAY_MESSAGE = 'Look!' scenarios = [('click_app_with_version', dict(app_name="com.ubuntu.developer.webapps.webapp-twitter_webapp-twitter", appid=None, path=None, desktop_dir="~/.local/share/applications/", icon="twitter", launcher_idx=5)), ('click_app', dict(app_name="com.ubuntu.developer.webapps.webapp-twitter_webapp-twitter", appid="com.ubuntu.developer.webapps.webapp-twitter_webapp-twitter", path="com_2eubuntu_2edeveloper_2ewebapps_2ewebapp_2dtwitter", desktop_dir="~/.local/share/applications/", icon="twitter", launcher_idx=5)), ('legacy_app', dict(app_name="messaging-app", appid="_messaging-app", path="_", desktop_dir="/usr/share/applications/", icon="messages-app", launcher_idx=1))] def setUp(self): super(TestPushClientUnicast, self).setUp() # only for the click_app_with_version scenario if self.path is None and self.appid is None: self.appid, self.path = self._get_click_appid_and_path() self.token = self.push_helper.register(self.path, self.appid) def _get_click_appid_and_path(self): """Return the click appid including the version and dbus path.""" # get appid with version from the .desktop file list. files = os.listdir(os.path.expanduser(self.desktop_dir)) for fname in files: if fname.startswith(self.app_name) and \ fname.endswith(".desktop"): # remove .desktop extension, only need the name. appid = os.path.splitext(fname)[0] path = appid.split("_")[0] path = path.replace(".", "_2e").replace("-", "_2d") return appid, path return self.appid, self.path @skipIf(platform.model() == 'X86 Emulator', "Test not working in the emulator") def test_unicast_push_notification_persistent(self): """Send a persistent unicast push notification. Notification should be displayed in the incoming indicator. """ # Assumes greeter starts in locked state self.unlock_greeter() # send message self.send_unicast_notification(persist=True, popup=False, icon=self.icon) self.validate_mmu_notification("A unicast message", "Look!") def get_running_app_launcher_icon(self): launcher = self.main_window.get_launcher() return launcher.select_single( 'LauncherDelegate', objectName='launcherDelegate%d' % self.launcher_idx) def test_unicast_push_notification_emblem_count(self): """Send a emblem-counter enabled unicast push notification. Notification should be displayed at the dash/emblem. """ # Assumes greeter starts in locked state self.unlock_greeter() # open the app, only if isn't by default in the launcher_id if self.launcher_idx >= 4: try: self.launch_upstart_application( self._get_click_appid_and_path()[0]) except Exception: # ignore dbus instrospection errors pass # move the app to the background self.main_window.show_dash_swiping() # check the icon has no emblems app_icon = self.get_running_app_launcher_icon() self.assertEqual(app_icon.count, 0) # send message, only showing emblem counter emblem_counter = {'count': 42, 'visible': True} self.send_unicast_notification(persist=False, popup=False, icon=self.icon, emblem_counter=emblem_counter) # show the dash and check the emblem count. self.main_window.show_dash_swiping() # check there is a emblem count == 2 app_icon = self.get_running_app_launcher_icon() self.assertEqual(app_icon.count, emblem_counter['count']) def test_unicast_push_notification_locked_greeter(self): """Send a push notification while in the greeter scrren. The notification should be displayed on top of the greeter. """ # Assumes greeter starts in locked state self.send_unicast_notification(summary="Locked greeter", icon=self.icon) self.validate_and_dismiss_notification_dialog("Locked greeter") def test_unicast_push_notification(self): """Send a push notificationn and validate it's displayed.""" # Assumes greeter starts in locked state self.unlock_greeter() self.send_unicast_notification(icon=self.icon) self.validate_and_dismiss_notification_dialog( self.DEFAULT_DISPLAY_MESSAGE) def test_unicast_push_notification_on_connect(self): """Send a unicast notification whilst the push client is disconnected. Then reconnect and ensure message is displayed. """ # Assumes greeter starts in locked state self.unlock_greeter() self.push_client_controller.stop_push_client() self.send_unicast_notification(icon=self.icon) self.push_client_controller.start_push_client() self.validate_and_dismiss_notification_dialog( self.DEFAULT_DISPLAY_MESSAGE) def test_expired_unicast_push_notification(self): """Send an expired unicast notification message to server.""" # Assumes greeter starts in locked state self.unlock_greeter() # create notification message using past expiry time expire_on = self.push_helper.get_past_iso_time() # XXX: build a unicast message notif = {"notification": {"card": {"icon": "messages-app", "summary": "A summary", "body": "The body of the msg.", "popup": True, "actions": [] } } } data = {'token': self.token, 'data': notif, 'appid': self.appid, 'expire_on': expire_on} # send message response = self.push_helper.send_unicast( data, self.test_config.server_listener_addr) # 400 status is received for an expired message self.validate_response(response, expected_status_code=400) # validate no notification is displayed self.validate_notification_not_displayed() ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/README0000644000015600001650000000601412670364255026430 0ustar pbuserpbgroup00000000000000================== README ================== To run ubuntu-push autopilot tests you need to have a push server available. This can be running locally using loopback (127.0.0.1) or remotely on the same network. ---------------------------------- To configure and build the server: ---------------------------------- 1) export GOPATH=${PWD}/push 2) mkdir -p push/src/launchpad.net 3) cd push/src/launchpad.net 4) bzr branch lp:ubuntu-push 5) Edit ubuntu-push/sampleconfigs/dev.json with the correct server IP address and ports. E.g. for a server IP address 192.168.1.2 use: "addr": "192.168.1.2:9090", "http_addr": "192.168.1.2:8080", 6) cd ubuntu-push 7) ensure all dependencies are installed: sudo apt-get build-dep ubuntu-push 8) install additional cm tools: sudo apt-get install git mercurial 9) make bootstrap 10) make run-server-dev Following output should be observed: INFO listening for http on 192.168.1.2:8080 INFO listening for devices on 192.168.1.2:9090 ------------------------ To configure the client: ------------------------ Install depenendencies: 1) sudo apt-get install unity8-autopilot 2) bzr branch lp:ubuntu-push 3) Edit ip address and ports to match environment: ubuntu-push/tests/autopilot/push_notifications/config/push.conf: [config] addr = 192.168.1.2 listener_port = 8080 device_port = 9090 cert_pem_file = testing.cert 4) initctl stop unity8 5) cd ubuntu-push/tests/autopilot 6) autopilot3 list push_notifications 7) autopilot3 run push_notifications 8) To run a specific test case use the test case identifier from the list command: - e.g. autopilot3 run push_notifications.tests.test_broadcast_notifications.TestPushClientBroadcast.test_broadcast_push_notification ---------------- Troubleshooting: ---------------- 1) Ping from client to server to ensure connectivity is correct 2) Delete ~/.local/share/ubuntu-push-client/levels.db if no notifications are being displayed: rm ~/.local/share-ubuntu-push-client/levels.db 3) Check client log file at ~/.cache/upstart/ubuntu-push-client.log: tail -f --line=30 ~/.cache/upstart/ubuntu-push-client.log 4) To send a notification manually: echo '{"channel":"system", "data": {"ubuntu-touch/utopic-proposed/mako": [94, ""]}, "expire_on": "2015-12-19T16:39:57-08:00"}' | POST -c application/json http://192.168.1.2:8080/broadcast Response should be: {"ok":true} Note that: - The channel and device names must match the client. - The build number must be greater than current installed version in order to trigger an update message. - The expiration time must be in the future. 5) Ensure unity8-autopilot is installed 6) Ensure unity8 is not running before executing the tests: - initctl stop unity8 7) Unity8 has a 2 minute timeout period, so stopping and starting it can take up to this long. 8) If device/emulator is unresponsive then reboot and stop unity8 before re-running the tests (initctl stop unity8). 9) To get additional autopilot logging use -v option: - autopilot3 run -v push_notifications ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/data.py0000644000015600001650000000612212670364255027033 0ustar pbuserpbgroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Push Notifications Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . # """Push-Notifications autopilot data structure classes""" class PushNotificationMessage: """ Class to hold all the details required for a push notification message """ def __init__(self, channel='system', data=None, expire_after=None): """ Constructor :param channel: Name of channel :param data: Data value :param expire_after: expiration time """ self.channel = channel self.data = data self.expire_after = expire_after def to_json(self): """ Return JSON representation of message :return: JSON representation of message """ json_str = '{{"channel":"{0}", "data":{{{1}}}, "expire_on":"{2}"}}' return json_str.format(self.channel, self.data, self.expire_after) class DeviceNotificationData: """ Class to represent device's data used for sending notification, including: - Device software channel - Device build number - Device model - Device last update - Data for the notification """ def __init__(self, channel=None, device=None, build_number=None, last_update=None, version=None, data=None): """ Constructor :param channel: Name of channel :param device: Name of device :param build_number: Build number :param last_update: Last update time :param version: Build version :param data: Device specific data """ self.channel = channel self.build_number = build_number self.device = device self.last_update = last_update self.version = version self.data = data def inc_build_number(self): """ Increment build number """ self.build_number = str(int(self.build_number) + 1) def dec_build_number(self): """ Decrement build number """ self.build_number = str(int(self.build_number) - 1) def to_json(self): """ Return json representation of info based: "IMAGE-CHANNEL/DEVICE-MODEL": [BUILD-NUMBER, CHANNEL-ALIAS]" :return: JSON representation of device data """ json_str = '"{0}/{1}": [{2}, "{3}"]' return json_str.format(self.channel, self.device, self.build_number, self.data) ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/helpers/0000755000015600001650000000000012670364532027207 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/helpers/__init__.py0000644000015600001650000000142412670364255031323 0ustar pbuserpbgroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Push Notifications Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . # ././@LongLink0000000000000000000000000000015200000000000011213 Lustar 00000000000000ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/helpers/push_notifications_helper.pyubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/helpers/push_notifications_help0000644000015600001650000002672012670364255034063 0ustar pbuserpbgroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Push Notifications Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . # import configparser import datetime import http.client as http import json import os import subprocess import systemimage.config as sys_info from push_notifications import config as push_config from push_notifications.data import ( PushNotificationMessage, DeviceNotificationData ) class PushClientConfig: """ Container class to read and hold all required server config - Server listener address - Server device address - Certificate PEM file path """ @staticmethod def read_config(config_file_path): """ Return PushClientConfig object containing all test config parameters which have been read from specified config file. :param config_file_path: path to required config file :return: PushClientConfig object containing all config parameters """ KEY_ADDR = 'addr' KEY_LISTENER_PORT = 'listener_port' KEY_DEVICE_PORT = 'device_port' KEY_CONFIG = 'config' KEY_CERT_PEM_FILE = 'cert_pem_file' KEY_AUTH_HELPER = 'auth_helper' config = PushClientConfig() parser = configparser.ConfigParser() parser.read(config_file_path) server_addr = parser[KEY_CONFIG][KEY_ADDR] device_port = parser[KEY_CONFIG][KEY_DEVICE_PORT] listener_port = parser[KEY_CONFIG][KEY_LISTENER_PORT] auth_helper = parser[KEY_CONFIG][KEY_AUTH_HELPER] addr_fmt = '{0}:{1}' http_addr_fmt = 'http://{0}:{1}/' config.server_listener_addr = addr_fmt.format( server_addr, listener_port) config.server_device_addr = addr_fmt.format(server_addr, device_port) config.server_session_url = http_addr_fmt.format(server_addr, listener_port) config.server_registration_url = http_addr_fmt.format(server_addr, listener_port) config.cert_pem_file = push_config.get_cert_file( parser[KEY_CONFIG][KEY_CERT_PEM_FILE]) config.auth_helper = auth_helper return config class PushClientController: """ Class used to reconfigure and re-start ubuntu-push-client for testing """ PUSH_CLIENT_DEFAULT_CONFIG_FILE = '/etc/xdg/ubuntu-push-client/config.json' PUSH_CLIENT_CONFIG_FILE = '~/.config/ubuntu-push-client/config.json' def restart_push_client_using_config(self, client_config=None): """ Restart the push client using the config provided If the config is none then revert to default client behaviour :param client_config: PushClientConfig object containing required config """ if client_config is None: # just delete the local custom config file # client will then just use the original config abs_config_file = self.get_abs_local_config_file_path() if os.path.exists(abs_config_file): os.remove(abs_config_file) else: # write the config to local config file self.write_client_test_config(client_config) # Now re-start the client self.restart_push_client() def write_client_test_config(self, client_config): """ Write the test server address and certificate path to the client config file :param client_config: PushClientConfig object containing required config """ # read the original push client config file with open(self.PUSH_CLIENT_DEFAULT_CONFIG_FILE) as config_file: config = json.load(config_file) # change server address config['addr'] = client_config.server_device_addr # change session_url config['session_url'] = client_config.server_session_url # change registration url config['registration_url'] = client_config.server_registration_url # add certificate file path config['cert_pem_file'] = client_config.cert_pem_file # change the auth_helper config['auth_helper'] = client_config.auth_helper # write the config json out to the ~.local address # creating the directory if it doesn't already exist abs_config_file = self.get_abs_local_config_file_path() config_dir = os.path.dirname(abs_config_file) if not os.path.exists(config_dir): os.makedirs(config_dir) with open(abs_config_file, 'w+') as outfile: json.dump(config, outfile, indent=4) outfile.close() def get_abs_local_config_file_path(self): """ Return absolute path of ~.local config file """ return os.path.expanduser(self.PUSH_CLIENT_CONFIG_FILE) def control_client(self, command): """ start/stop/restart the ubuntu-push-client using initctl """ subprocess.call( ['/sbin/initctl', command, 'ubuntu-push-client'], stdout=subprocess.DEVNULL) def stop_push_client(self): """ Stop the push client """ self.control_client('stop') def start_push_client(self): """ Start the push client """ self.control_client('start') def restart_push_client(self): """ Restart the push client """ self.stop_push_client() self.start_push_client() class PushNotificationHelper: """ Utility class to create and send push notification messages """ DEFAULT_BROADCAST_URL = '/broadcast' UNICAST_URL = '/notify' def get_device_info(self): """ Discover the device's model and build info - device name e.g. mako - channel name e.g. ubuntu-touch/utopic-proposed - build_number e.g. 101 :return: DeviceNotificationData object containing device info """ # channel info needs to be read from file parser = configparser.ConfigParser() channel_config_file = '/etc/system-image/channel.ini' parser.read(channel_config_file) channel = parser['service']['channel'] return DeviceNotificationData( device=sys_info.config.device, channel=channel, build_number=sys_info.config.build_number) def send_push_broadcast_notification(self, msg_json, server_addr, url=DEFAULT_BROADCAST_URL): """ Send the specified push message to the server broadcast url using an HTTP POST command :param msg_json: JSON representation of message to send :param url: destination server url to send message to """ headers = {'Content-type': 'application/json'} conn = http.HTTPConnection(server_addr) conn.request( 'POST', url, headers=headers, body=msg_json) return conn.getresponse() def create_push_message(self, channel='system', data=None, expire_after=None): """ Return a new push message If no expiry time is given, a future date will be assigned :param channel: name of the channel :param data: data value of the message :param expire_after: expiry time for message :return: PushNotificationMessage object containing specified parameters """ if expire_after is None: expire_after = self.get_future_iso_time() return PushNotificationMessage( channel=channel, data=data, expire_after=expire_after) def get_past_iso_time(self): """ Return time 1 year in past in ISO format """ return self.get_iso_time(year_offset=-1) def get_near_past_iso_time(self): """ Return time 5 seconds in past in ISO format """ return self.get_iso_time(sec_offset=-5) def get_near_future_iso_time(self): """ Return time 5 seconds in future in ISO format """ return self.get_iso_time(sec_offset=5) def get_future_iso_time(self): """ Return time 1 year in future in ISO format """ return self.get_iso_time(year_offset=1) def get_current_iso_time(self): """ Return current time in ISO format """ return self.get_iso_time() def get_iso_time(self, year_offset=0, month_offset=0, day_offset=0, hour_offset=0, min_offset=0, sec_offset=0, tz_hour_offset=0, tz_min_offset=0): """ Return an ISO8601 format date-time string, including time-zone offset: YYYY-MM-DDTHH:MM:SS-HH:MM :param year_offset: number of years to offset :param month_offset: number of months to offset :param day_offset: number of days to offset :param hour_offset: number of hours to offset :param min_offset: number of minutes to offset :param sec_offset: number of seconds to offset :param tz_hour_offset: number of hours to offset time zone :param tz_min_offset: number of minutes to offset time zone :return: string representation of required time in ISO8601 format """ # calulate target time based on current time and format it now = datetime.datetime.now() target_time = datetime.datetime( year=now.year + year_offset, month=now.month + month_offset, day=now.day + day_offset, hour=now.hour + hour_offset, minute=now.minute + min_offset, second=now.second + sec_offset) target_time_fmt = target_time.strftime('%Y-%m-%dT%H:%M:%S') # format time zone offset tz = datetime.time( hour=tz_hour_offset, minute=tz_min_offset) tz_fmt = tz.strftime('%H:%M') # combine target time and time zone offset iso_time = '{0}-{1}'.format(target_time_fmt, tz_fmt) return iso_time def _http_request(self, server_addr, url, method='GET', body=None): headers = {'Content-type': 'application/json'} conn = http.HTTPConnection(server_addr) conn.request( method, url, headers=headers, body=body) return conn.getresponse() def register(self, path, appid): """Register the device/appid with the push server.""" cmd = ["gdbus", "call", "-e", "-d", "com.ubuntu.PushNotifications", "-o", "/com/ubuntu/PushNotifications/%s" % path, "-m", "com.ubuntu.PushNotifications.Register", appid] output = subprocess.check_output(cmd) return output[2:-4].decode("utf-8") def send_unicast(self, msg, server_addr, url=UNICAST_URL): """Send a unicast notification""" return self._http_request(server_addr, url, method='POST', body=json.dumps(msg)) ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/config/0000755000015600001650000000000012670364532027012 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/config/__init__.py0000644000015600001650000000226112670364255031126 0ustar pbuserpbgroup00000000000000# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # # Push Notifications Autopilot Test Suite # Copyright (C) 2014 Canonical # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . # import os CONFIG_FILE = 'push.conf' def get_config_file(): """ Return the path for the config file """ config_dir = os.path.dirname(__file__) return os.path.join(config_dir, CONFIG_FILE) def get_cert_file(cert_file_name): """ Return the path for the testing certificate file """ config_dir = os.path.dirname(__file__) return os.path.join(config_dir, cert_file_name) ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/config/push.conf0000644000015600001650000000027412670364255030645 0ustar pbuserpbgroup00000000000000[config] addr = 192.168.1.3 listener_port = 8080 device_port = 9090 cert_pem_file = testing.cert auth_helper = /home/phablet/ubuntu-push/src/launchpad.net/ubuntu-push/scripts/dummyauth.sh ubuntu-push-0.68+16.04.20160310.2/tests/autopilot/push_notifications/config/testing.cert0000644000015600001650000000103612670364255031350 0ustar pbuserpbgroup00000000000000-----BEGIN CERTIFICATE----- MIIBYzCCAQ+gAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD bzAeFw0xMzEyMTkyMDU1NDNaFw0yMzEyMTcyMDU1NDNaMBIxEDAOBgNVBAoTB0Fj bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAPw+niki17X2qALE2A2AzE1q5dvK 9CI4OduRtT9IgbFLC6psqAT21NA+QbY17nWSSpyP65zkMkwKXrbDzstwLPkCAwEA AaNUMFIwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud EwEB/wQFMAMBAf8wGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAsGCSqGSIb3 DQEBBQNBAFqiVI+Km2XPSO+pxITaPvhmuzg+XG3l1+2di3gL+HlDobocjBqRctRU YySO32W07acjGJmCHUKpCJuq9X8hpmk= -----END CERTIFICATE----- ubuntu-push-0.68+16.04.20160310.2/util/0000755000015600001650000000000012670364532017430 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/util/redialer_test.go0000644000015600001650000000571612670364255022620 0ustar pbuserpbgroup00000000000000/* 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 util import ( . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/bus" testibus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/testing/condition" "testing" "time" ) // hook up gocheck func TestRedialer(t *testing.T) { TestingT(t) } type RedialerSuite struct { timeouts []time.Duration } var _ = Suite(&RedialerSuite{}) func (s *RedialerSuite) SetUpSuite(c *C) { s.timeouts = SwapTimeouts([]time.Duration{0, 0}) } func (s *RedialerSuite) TearDownSuite(c *C) { SwapTimeouts(s.timeouts) s.timeouts = nil } // Redial() tests func (s *RedialerSuite) TestWorks(c *C) { endp := testibus.NewTestingEndpoint(condition.Fail2Work(3), nil) ar := NewAutoRedialer(endp) // c.Check(ar.(*autoRedialer).stop, NotNil) c.Check(ar.Redial(), Equals, uint32(4)) // and on success, the stopper goes away // c.Check(ar.(*autoRedialer).stop, IsNil) } func (s *RedialerSuite) TestRetryNil(c *C) { var ar *autoRedialer c.Check(ar.Redial, Not(PanicMatches), ".* nil pointer dereference") } func (s *RedialerSuite) TestRetryTwice(c *C) { endp := testibus.NewTestingEndpoint(condition.Work(true), nil) ar := NewAutoRedialer(endp) c.Check(ar.Redial(), Equals, uint32(1)) c.Check(ar.Redial(), Equals, uint32(0)) } type JitteringEndpoint struct { bus.Endpoint jittered int } func (j *JitteringEndpoint) Jitter(time.Duration) time.Duration { j.jittered++ return 0 } func (s *RedialerSuite) TestJitterWorks(c *C) { endp := &JitteringEndpoint{ testibus.NewTestingEndpoint(condition.Fail2Work(3), nil), 0, } ar := NewAutoRedialer(endp) c.Check(ar.Redial(), Equals, uint32(4)) c.Check(endp.jittered, Equals, 3) } // Stop() tests func (s *RedialerSuite) TestStopWorksOnNil(c *C) { // as a convenience, Stop() should succeed on nil // (a nil retrier certainly isn't retrying!) var ar *autoRedialer c.Check(ar, IsNil) ar.Stop() // nothing happens } func (s *RedialerSuite) TestStopStops(c *C) { endp := testibus.NewTestingEndpoint(condition.Work(false), nil) countCh := make(chan uint32) ar := NewAutoRedialer(endp) go func() { countCh <- ar.Redial() }() ar.Stop() select { case <-countCh: // pass case <-time.After(20 * time.Millisecond): c.Fatal("timed out waiting for redial") } // on Stop(), the redialer is Stopped c.Check(ar.(*autoRedialer).state(), Equals, Stopped) // and the next Stop() doesn't panic nor block ar.Stop() } ubuntu-push-0.68+16.04.20160310.2/util/redialer.go0000644000015600001650000001145412670364255021555 0ustar pbuserpbgroup00000000000000/* 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 util contains the redialer. package util import ( "sync" "time" ) // A Dialer is an object that knows how to establish a connection, and // where you'd usually want some kind of backoff if that connection // fails. type Dialer interface { Dial() error } // A Jitterer is a Dialer that wants to vary the backoff a little (to avoid a // thundering herd, for example). type Jitterer interface { Dialer Jitter(time.Duration) time.Duration } // The timeouts used during backoff. var timeouts []time.Duration var trwlock sync.RWMutex // Retrieve the list of timeouts used for exponential backoff. func Timeouts() []time.Duration { trwlock.RLock() defer trwlock.RUnlock() return timeouts } // For testing: change the default timeouts with the provided ones, // returning the defaults (the idea being you reset them on test // teardown). func SwapTimeouts(newTimeouts []time.Duration) (oldTimeouts []time.Duration) { trwlock.Lock() defer trwlock.Unlock() oldTimeouts, timeouts = timeouts, newTimeouts return } // An AutoRedialer's Redial() method retries its dialer's Dial() method until // it stops returning an error. It does exponential backoff (optionally // jittered). type AutoRedialer interface { Redial() uint32 // Redial keeps on calling Dial until it stops returning an error. Stop() // Stop shuts down the given AutoRedialer, if it is still retrying. } type redialerState uint32 const ( Unconfigured redialerState = iota Redialing Stopped ) func (s *redialerState) String() string { return [3]string{"Unconfigured", "Redialing", "Stopped"}[uint32(*s)] } type autoRedialer struct { stateLock sync.RWMutex stateValue redialerState stopping chan struct{} reallyStopped chan struct{} dial func() error jitter func(time.Duration) time.Duration } func (ar *autoRedialer) state() redialerState { ar.stateLock.RLock() defer ar.stateLock.RUnlock() return ar.stateValue } func (ar *autoRedialer) setState(s redialerState) { ar.stateLock.Lock() defer ar.stateLock.Unlock() ar.stateValue = s } func (ar *autoRedialer) setStateIfEqual(oldState, newState redialerState) bool { ar.stateLock.Lock() defer ar.stateLock.Unlock() if ar.stateValue != oldState { return false } ar.stateValue = newState return true } func (ar *autoRedialer) setStateStopped() { ar.stateLock.Lock() defer ar.stateLock.Unlock() switch ar.stateValue { case Stopped: return case Unconfigured: close(ar.reallyStopped) } ar.stateValue = Stopped close(ar.stopping) } func (ar *autoRedialer) Stop() { if ar != nil { ar.setStateStopped() <-ar.reallyStopped } } // Redial keeps on calling Dial until it stops returning an error. It does // exponential backoff, adding back the output of Jitter at each step. func (ar *autoRedialer) Redial() uint32 { if ar == nil { // at least it's better than a segfault... panic("you can't Redial a nil AutoRedialer") } if !ar.setStateIfEqual(Unconfigured, Redialing) { // XXX log this return 0 } defer close(ar.reallyStopped) var timeout time.Duration var dialAttempts uint32 = 0 // unsigned so it can wrap safely ... timeouts := Timeouts() var numTimeouts uint32 = uint32(len(timeouts)) for { if ar.state() != Redialing { return dialAttempts } if ar.dial() == nil { return dialAttempts + 1 } if dialAttempts < numTimeouts { timeout = timeouts[dialAttempts] } else { timeout = timeouts[numTimeouts-1] } if ar.jitter != nil { timeout += ar.jitter(timeout) } dialAttempts++ select { case <-ar.stopping: case <-time.After(timeout): } } } // Returns a stoppable AutoRedialer using the provided Dialer. If the Dialer // is also a Jitterer, the backoff will be jittered. func NewAutoRedialer(dialer Dialer) AutoRedialer { ar := &autoRedialer{ stateValue: Unconfigured, dial: dialer.Dial, reallyStopped: make(chan struct{}), stopping: make(chan struct{}), } jitterer, ok := dialer.(Jitterer) if ok { ar.jitter = jitterer.Jitter } return ar } func init() { ps := []int{1, 2, 5, 11, 19, 37, 67, 113, 191} // 3 pₙ₊₠≥ 5 pâ‚™ timeouts := make([]time.Duration, len(ps)) for i, n := range ps { timeouts[i] = time.Duration(n) * time.Second } SwapTimeouts(timeouts) } ubuntu-push-0.68+16.04.20160310.2/util/redialer_states.gv0000644000015600001650000000044212670364255023142 0ustar pbuserpbgroup00000000000000digraph "redialer" { "Unconfigured" -> "Redialing" [ label="Redial" ] "Unconfigured" -> "Stopped" [ label="Stop" ] "Redialing" -> "Redialing" [ label="Redial" ] "Redialing" -> "Stopped" [ label="Stop" ] "Stopped" -> "Stopped" [ label="*" ] } ubuntu-push-0.68+16.04.20160310.2/README0000644000015600001650000000234312670364255017337 0ustar pbuserpbgroup00000000000000Ubuntu Push Notifications ------------------------- Protocol, client, and development code for Ubuntu Push Notifications. The code expects to be checked out as launchpad.net/ubuntu-push in a Go workspace, see "go help gopath". You need a somewhat long list of dependencies, as well as a working Go development environment. THe Ubuntu packagenames for these are listed in the file PACKAGE_DEPS. On Ubuntu, if you have sudo, you can have all those installed for you by do doing make fetchdeps Once you have the packaged dependencies you can get the Go dependencies via make bootstrap and then you're set. Good luck! To run the tests: make check To produce coverage reports you need Go 1.2 (default on Trusty) and the cover tool (in the golang-go.tools package), then run: make coverage-summary for a summary report, or: for per-package HTML with annotated code in coverhtml/.html make coverage-html (it makes also textual coverhtml/.txt reports). To run the acceptance tests, change to the acceptance subdir and run: make acceptance There are build targets to build the client: make build-client building ubuntu-push-client, and to run the development server: make run-server-dev ubuntu-push-0.68+16.04.20160310.2/sounds/0000755000015600001650000000000012670364532017766 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/sounds/sounds.go0000644000015600001650000000753612670364255021645 0ustar pbuserpbgroup00000000000000/* 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 sounds import ( "errors" "os" "os/exec" "path/filepath" "strings" "launchpad.net/go-xdg/v0" "launchpad.net/ubuntu-push/bus/accounts" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/logger" ) type Sound interface { // Present() presents the notification audibly if applicable. Present(app *click.AppId, nid string, notification *launch_helper.Notification) bool // GetSound() returns absolute path to the file the given notification will play. GetSound(app *click.AppId, nid string, notification *launch_helper.Notification) string } type sound struct { player string log logger.Logger acc accounts.Accounts fallback string dataDirs func() []string dataFind func(string) (string, error) } func New(log logger.Logger, acc accounts.Accounts, fallback string) *sound { return &sound{ player: "paplay", log: log, acc: acc, fallback: fallback, dataDirs: xdg.Data.Dirs, dataFind: xdg.Data.Find, } } func (snd *sound) Present(app *click.AppId, nid string, notification *launch_helper.Notification) bool { if notification == nil { panic("please check notification is not nil before calling present") } absPath := snd.GetSound(app, nid, notification) if absPath == "" { return false } snd.log.Debugf("[%s] playing sound %s using %s", nid, absPath, snd.player) cmd := exec.Command(snd.player, absPath) err := cmd.Start() if err != nil { snd.log.Debugf("[%s] unable to play: %v", nid, err) return false } go func() { err := cmd.Wait() if err != nil { snd.log.Debugf("[%s] error playing sound %s: %v", nid, absPath, err) } }() return true } // Returns the absolute path of the sound to be played for app, nid and notification. func (snd *sound) GetSound(app *click.AppId, nid string, notification *launch_helper.Notification) string { if snd.acc.SilentMode() { snd.log.Debugf("[%s] no sounds: silent mode on.", nid) return "" } fallback := snd.acc.MessageSoundFile() if fallback == "" { fallback = snd.fallback } sound := notification.Sound(fallback) if sound == "" { snd.log.Debugf("[%s] notification has no Sound: %#v", nid, sound) return "" } absPath := snd.findSoundFile(app, nid, sound) if absPath == "" { snd.log.Debugf("[%s] unable to find sound %s", nid, sound) } return absPath } // Removes all cruft from path, ensures it's a "forward" path. func (snd *sound) cleanPath(path string) (string, error) { cleaned := filepath.Clean(path) if strings.Contains(cleaned, "../") { return "", errors.New("Path escaping xdg attempt") } return cleaned, nil } func (snd *sound) findSoundFile(app *click.AppId, nid string, sound string) string { // XXX also support legacy appIds? // first, check package-specific sound, err := snd.cleanPath(sound) if err != nil { // bad boy return "" } absPath, err := snd.dataFind(filepath.Join(app.Package, sound)) if err == nil { // ffffound return absPath } // next, check the XDG data dirs (but skip the first one -- that's "home") // XXX should we only check in $XDG/sounds ? (that's for sound *themes*...) for _, dir := range snd.dataDirs()[1:] { absPath := filepath.Join(dir, sound) _, err := os.Stat(absPath) if err == nil { return absPath } } return "" } ubuntu-push-0.68+16.04.20160310.2/sounds/sounds_test.go0000644000015600001650000001221312670364255022670 0ustar pbuserpbgroup00000000000000/* 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 sounds import ( "encoding/json" "errors" "os" "path" "testing" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/click" clickhelp "launchpad.net/ubuntu-push/click/testing" "launchpad.net/ubuntu-push/launch_helper" helpers "launchpad.net/ubuntu-push/testing" ) func TestSounds(t *testing.T) { TestingT(t) } type soundsSuite struct { log *helpers.TestLogger app *click.AppId acc *mockAccounts } var _ = Suite(&soundsSuite{}) type mockAccounts struct { vib bool sil bool snd string err error } func (m *mockAccounts) Start() error { return m.err } func (m *mockAccounts) Cancel() error { return m.err } func (m *mockAccounts) SilentMode() bool { return m.sil } func (m *mockAccounts) Vibrate() bool { return m.vib } func (m *mockAccounts) MessageSoundFile() string { return m.snd } func (m *mockAccounts) String() string { return "" } func (ss *soundsSuite) SetUpTest(c *C) { ss.log = helpers.NewTestLogger(c, "debug") ss.app = clickhelp.MustParseAppId("com.example.test_test_0") ss.acc = &mockAccounts{true, false, "", nil} } func (ss *soundsSuite) TestNew(c *C) { s := New(ss.log, ss.acc, "foo") c.Check(s.log, Equals, ss.log) c.Check(s.player, Equals, "paplay") c.Check(s.fallback, Equals, "foo") c.Check(s.acc, Equals, ss.acc) } func (ss *soundsSuite) TestPresent(c *C) { s := &sound{ player: "echo", log: ss.log, acc: ss.acc, dataFind: func(s string) (string, error) { return s, nil }, } c.Check(s.Present(ss.app, "", &launch_helper.Notification{RawSound: json.RawMessage(`"hello"`)}), Equals, true) c.Check(ss.log.Captured(), Matches, `(?sm).* playing sound com.example.test/hello using echo`) } func (ss *soundsSuite) TestPresentSimple(c *C) { s := &sound{ player: "echo", log: ss.log, acc: ss.acc, dataFind: func(s string) (string, error) { return s, nil }, fallback: "fallback", } c.Check(s.Present(ss.app, "", &launch_helper.Notification{RawSound: json.RawMessage(`true`)}), Equals, true) c.Check(ss.log.Captured(), Matches, `(?sm).* playing sound com.example.test/fallback using echo`) ss.acc.snd = "from-prefs" ss.log.ResetCapture() c.Check(s.Present(ss.app, "", &launch_helper.Notification{RawSound: json.RawMessage(`true`)}), Equals, true) c.Check(ss.log.Captured(), Matches, `(?sm).* playing sound com.example.test/from-prefs using echo`) } func (ss *soundsSuite) TestPresentFails(c *C) { s := &sound{ player: "/", log: ss.log, acc: ss.acc, dataFind: func(string) (string, error) { return "", errors.New("nope") }, dataDirs: func() []string { return []string{""} }, } // nil notification c.Check(func() { s.Present(ss.app, "", nil) }, Panics, `please check notification is not nil before calling present`) // no Sound c.Check(s.Present(ss.app, "", &launch_helper.Notification{}), Equals, false) // bad player c.Check(s.Present(ss.app, "", &launch_helper.Notification{RawSound: json.RawMessage(`"hello"`)}), Equals, false) s.player = "echo" // no file found c.Check(s.Present(ss.app, "", &launch_helper.Notification{RawSound: json.RawMessage(`"hello"`)}), Equals, false) // and now, just to prove it would've worked, d := c.MkDir() f, err := os.Create(path.Join(d, "hello")) c.Assert(err, IsNil) f.Close() s.dataDirs = func() []string { return []string{"", d} } c.Check(s.Present(ss.app, "", &launch_helper.Notification{RawSound: json.RawMessage(`"hello"`)}), Equals, true) } func (ss *soundsSuite) TestBadPathFails(c *C) { s := &sound{ player: "/", log: ss.log, acc: ss.acc, dataFind: func(string) (string, error) { return "", errors.New("nope") }, dataDirs: func() []string { return []string{""} }, } sound, err := s.cleanPath("../../foo") c.Check(err, NotNil) c.Check(sound, Equals, "") } func (ss *soundsSuite) TestGoodPathSucceeds(c *C) { s := &sound{ player: "/", log: ss.log, acc: ss.acc, dataFind: func(string) (string, error) { return "", errors.New("nope") }, dataDirs: func() []string { return []string{""} }, } sound, err := s.cleanPath("foo/../bar") c.Check(err, IsNil) c.Check(sound, Equals, "bar") } func (ss *soundsSuite) TestSkipIfSilentMode(c *C) { s := &sound{ player: "echo", log: ss.log, acc: ss.acc, fallback: "fallback", dataFind: func(s string) (string, error) { return s, nil }, } c.Check(s.Present(ss.app, "", &launch_helper.Notification{RawSound: json.RawMessage(`true`)}), Equals, true) ss.acc.sil = true c.Check(s.Present(ss.app, "", &launch_helper.Notification{RawSound: json.RawMessage(`true`)}), Equals, false) } ubuntu-push-0.68+16.04.20160310.2/COPYING0000644000015600001650000010451312670364255017514 0ustar pbuserpbgroup00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ubuntu-push-0.68+16.04.20160310.2/testing/0000755000015600001650000000000012670364532020130 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/testing/tls.go0000644000015600001650000001471112670364255021267 0ustar pbuserpbgroup00000000000000/* 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 import ( "crypto/tls" "crypto/x509" "io/ioutil" ) // key&cert generated with go run /usr/lib/go/src/pkg/crypto/tls/generate_cert.go -ca -host push-delivery -rsa-bits 512 -duration 87600h var ( TestKeyPEMBlock = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIBOgIBAAJBANRU+pZKMNHpMvg549meJ060xQ4HCjrfVq+AeIER9W1pkaknDj8c hwOWKHTeztcPF/LHVpKPabn+fSNbFlq+SzcCAwEAAQJBAIOO+4xu/3yv/rKqO7C0 Oyqa+pVMa1w60R0AfqmKFQTqiTgevM77uqjpW1+t0hpK20nyj6MUIPaL+9kZgp7t mnECIQDqw79PXSzudf10XGy9ve5bRazINHxQYgJ7FvlTT6JhdQIhAOeJxq9zcKni 69ueO1ualz0hn8w6uHPsG9FlZ8C+7Jh7AiAWJgebjjfZ+4nA+6NKt2uQct9dOA5u awC+6ij1ojK4rQIgNEqAbcWDj0qpe8sLms+aEntSjJxCZiPP0IW3XeeApZsCIDwo x+YyxXQWJlf9L5TNYPRo+KFEdk3Cew0lv6QNs+xe -----END RSA PRIVATE KEY-----`) TestCertPEMBlock = []byte(`-----BEGIN CERTIFICATE----- MIIBYzCCAQ+gAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD bzAeFw0xNDA4MjkxMjQyMDFaFw0yNDA4MjYxMjQyMDFaMBIxEDAOBgNVBAoTB0Fj bWUgQ28wXDANBgkqhkiG9w0BAQEFAANLADBIAkEA1FT6lkow0eky+Dnj2Z4nTrTF DgcKOt9Wr4B4gRH1bWmRqScOPxyHA5YodN7O1w8X8sdWko9puf59I1sWWr5LNwID AQABo1IwUDAOBgNVHQ8BAf8EBAMCAKQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYD VR0TAQH/BAUwAwEB/zAYBgNVHREEETAPgg1wdXNoLWRlbGl2ZXJ5MAsGCSqGSIb3 DQEBBQNBABtWCdMFkhIO8+oM3vugOWle9WJZ1FCRWD+cMl76mI1lhmNF4lvEZG47 xUjekA1+heU39WpOEzZSybrOdiEaGbI= -----END CERTIFICATE-----`) // key&cert generated with openssl req -x509 -nodes -newkey rsa:2048 // -multivalue-rdn -sha512 -days 3650 -keyout testing.key -out // testing.cert -subj "/O=Acme Co/CN=push-delivery/" TestKeyPEMBlock512 = []byte(`-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4ySO/avJFWps8 AygUZ0dcylNr1UxZb4QPHuO93OXAkYX5ngw7TjnWIGHjvoLzLzPZCxlrGl7e+M1H GNZqFT3kFv/XYexp9Cx3MCDy0ZWkK9BAVDTAxMkjSR8ZwRjByQqniilDA/kr92NQ yaL0GlajsxpmcGMjDM0Dp5QF+inQM48ADJpJl0xlfFwE8CwfVVGM8G/ZtQpBJ3AN RelEG1iF8tsT9nVlWF37Zp9Wp/CxDDVTuzboZx9pkryOeJmm0l93x1aoSy6DTVyg zjdAOjKFjSsjY7we7x7GgpHuUtXymVH7OHdc0ji5+2O+yf9VEDxuym0fJJEgVLfX ungSHFxJAgMBAAECggEAeC2gyTqF7KM7+LDY3UQ6Plf8H1KvAC+txKPDXFURO8ep SaoHrH540RFoeNULl5uobc1xL54L+5n27/lwYbgE85YduHegaVx7mty7YRD78LTq ERxy3rhdVEyXJInYTxgwjLwnj8VCxdx0RDOPfpCurnKqhdssLryBjZHsjGKh1RzH bv5fNrqMhU0uH82cOKXy20uzyVo5zuLwWA+PxCEeOTMumpWgN4PmtMrjUot2t2/q jVoEkrB3B5Xs/s8OrEv10t90nNQPcKT89Kts/jdmgDNNg/dtILogiD4JshTG8fIB STUArRDCE0NXOmB0XuXRxk8YlZyBj2AsIUQcFRrOgQKBgQDrAkE77wIZcCJRYGxK KkB5zE5Lei44dKEHU5zIueOflsWFC+RZGWVn1+hTQw9Sk1kqm5atrDbfMZDOk62U bNcQLT+QqDRo3iSYLo9Q5hFNNxMGUm6RMHApr5iIZeoBFDZ7b4+zCEEFNtYukvjY DWyeTgUqftoOTDebHbHrk9w/0QKBgQDJSnnestarqjLXyF4RWzcFTsDjFgRv53Cq WrpiQUkk5JLlKliwoTAGTxzH2skJofT6OAQjrc5489mc5Gt6TVwWB49l+OzzG4H/ QSe5X9I5BEEcdD27wDwsaO/NsusM9jZ4IjauTKR5XqGoepbrWrm7+lBgEe1DvBWx C71U7Eoq+QKBgQDNJT2+zMf/XrSGZu6A21tHN0KNfo2EeMLsu19clXCPKjUoDBZ8 dL/ho0bKD/r7MWcf24vv9So9MW5f9egLbeta0rTvWPXPKUO2mMZAb2VhCxePaDve f/MZYJB9WMGpyXQ50kwVk7n2jETxiRiyuR09H4xA6VT+MChGPujGZV9ZUQKBgH7i 06/uTCQqRaKAS8vlE+nkmvKLDoD8A6lfR95oCROYgoCzEPVGpl9Tv3C8Gb5YuXSB mxpilaTpEmQ0GQwfd8zrNxmwsK0OygN9ruzL2ljWtbSaEdAofcYA4Clqf4DMM8nG x3FYHtXjMURjAn+Z0TsNr1zf8BCin4nbPJ4r1RUBAoGBALFHLtEWwVxpm3MN4f08 GtH2Phd289H0s5SaX/NaWYy44T+Q/d7LuYk72LWX1jZB/2V3OhiFzih0uK44PBM4 Gaiu8c/vl+M1hixeOenTrapE4ORaYt76INIEC8JpqEvGi0DYkUH1D4F8zzAiejgF t+nz90UBRCRA8vtZ8fiwz8O0 -----END PRIVATE KEY-----`) TestCertPEMBlock512 = []byte(`-----BEGIN CERTIFICATE----- MIIDJzCCAg+gAwIBAgIJAP9ScfFaKlalMA0GCSqGSIb3DQEBDQUAMCoxEDAOBgNV BAoMB0FjbWUgQ28xFjAUBgNVBAMMDXB1c2gtZGVsaXZlcnkwHhcNMTUwNDE1MTYx MDM1WhcNMjUwNDEyMTYxMDM1WjAqMRAwDgYDVQQKDAdBY21lIENvMRYwFAYDVQQD DA1wdXNoLWRlbGl2ZXJ5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA uMkjv2ryRVqbPAMoFGdHXMpTa9VMWW+EDx7jvdzlwJGF+Z4MO0451iBh476C8y8z 2QsZaxpe3vjNRxjWahU95Bb/12HsafQsdzAg8tGVpCvQQFQ0wMTJI0kfGcEYwckK p4opQwP5K/djUMmi9BpWo7MaZnBjIwzNA6eUBfop0DOPAAyaSZdMZXxcBPAsH1VR jPBv2bUKQSdwDUXpRBtYhfLbE/Z1ZVhd+2afVqfwsQw1U7s26GcfaZK8jniZptJf d8dWqEsug01coM43QDoyhY0rI2O8Hu8exoKR7lLV8plR+zh3XNI4uftjvsn/VRA8 bsptHySRIFS317p4EhxcSQIDAQABo1AwTjAdBgNVHQ4EFgQUG2Qk9GbWWfSPXRTE +cfOZMljydAwHwYDVR0jBBgwFoAUG2Qk9GbWWfSPXRTE+cfOZMljydAwDAYDVR0T BAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOCAQEAUw36s8n8a39ECYUmSS5o+PdjmF1v 6K6ld5n7IlFVwCtA1Rkz2L2AUrko/ao1/ZgKhHsIBFQ7mm5fkvuNd14ZEJ0F8LyI 55Et63IYWYOPHl0oNmzTHex0WRL9nmNvxbQ5UytzGTE5amv/sZTOYH9qnpEes68O TPP+C3OoM+U6hjOXNGG73zb54JHQUZ4arMg2gbVzxNXU2ReoKYKrYexGGuqIlHcE XdOQp93oJfqWAj111YS6tIn63ccjx7bKzFzaufuVvCIsk0WrXG2rpuqx+0OYzRKc deU3hnONgWVXjCQdNysBzUXLeOWcv1KuqScETvGZe7D1UIk7HWsAgnQnYQ== -----END CERTIFICATE-----`) // key&cert, same as server/acceptance/ssl/testing.* TestKeyPEMBlockAcceptance []byte TestCertPEMBlockAcceptance []byte ) // test tls server & client configs var ( TestTLSServerConfigs = map[string]*tls.Config{} TestTLSClientConfigs = map[string]*tls.Config{} TestTLSServerConfig, TestTLSClientConfig *tls.Config ) func init() { var err error TestKeyPEMBlockAcceptance, err = ioutil.ReadFile(SourceRelative("../server/acceptance/ssl/testing.key")) if err != nil { panic(err) } TestCertPEMBlockAcceptance, err = ioutil.ReadFile(SourceRelative("../server/acceptance/ssl/testing.cert")) if err != nil { panic(err) } for _, cfgBits := range []struct { label string key []byte cert []byte }{ {"sha1", TestKeyPEMBlock, TestCertPEMBlock}, {"sha512", TestKeyPEMBlock512, TestCertPEMBlock512}, {"acceptance", TestKeyPEMBlockAcceptance, TestCertPEMBlockAcceptance}, } { cert, err := tls.X509KeyPair(cfgBits.cert, cfgBits.key) if err != nil { panic(err) } tlsServerConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, } cp := x509.NewCertPool() ok := cp.AppendCertsFromPEM(cfgBits.cert) if !ok { panic("failed to parse test cert") } tlsClientConfig := &tls.Config{ RootCAs: cp, ServerName: "push-delivery", } TestTLSClientConfigs[cfgBits.label] = tlsClientConfig TestTLSServerConfigs[cfgBits.label] = tlsServerConfig } TestTLSClientConfig = TestTLSClientConfigs["sha1"] TestTLSServerConfig = TestTLSServerConfigs["sha1"] } ubuntu-push-0.68+16.04.20160310.2/testing/condition/0000755000015600001650000000000012670364532022116 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/testing/condition/condition.go0000644000015600001650000000613112670364255024436 0ustar pbuserpbgroup00000000000000/* 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 condition implements a strategy family for use in testing. package condition import ( "fmt" "strings" "sync" ) type Interface interface { OK() bool String() string } // Work is a simple boolean condition; either it works all the time // (when true), or it fails all the time (when false). func Work(wk bool) work { return work(wk) } type work bool func (c work) OK() bool { if c { return true } else { return false } } func (c work) String() string { if c { return "Always Working." } else { return "Never Working." } } var _ Interface = work(false) // Fail2Work fails for the first n times its OK() method is checked, // and then mysteriously starts working. func Fail2Work(left int32) *fail2Work { c := new(fail2Work) c.Left = left return c } type fail2Work struct { Left int32 lock sync.RWMutex } func (c *fail2Work) OK() bool { c.lock.Lock() defer c.lock.Unlock() if c.Left > 0 { c.Left-- return false } else { return true } } func (c *fail2Work) String() string { c.lock.RLock() defer c.lock.RUnlock() if c.Left > 0 { return fmt.Sprintf("Still Broken, %d to go.", c.Left) } else { return "Working." } } var _ Interface = &fail2Work{} // Not builds a condition that negates the one passed in. func Not(sub Interface) *not { return ¬{sub} } type not struct{ sub Interface } func (c *not) OK() bool { return !c.sub.OK() } func (c *not) String() string { return fmt.Sprintf("Not %s", c.sub) } var _ Interface = ¬{} type _iter struct { cond Interface remaining int } func (i _iter) String() string { return fmt.Sprintf("%d of %s", i.remaining, i.cond) } type chain struct { subs []*_iter lock sync.RWMutex } func (c *chain) OK() bool { var sub *_iter c.lock.Lock() defer c.lock.Unlock() for _, sub = range c.subs { if sub.remaining > 0 { sub.remaining-- return sub.cond.OK() } } return sub.cond.OK() } func (c *chain) String() string { ss := make([]string, len(c.subs)) c.lock.RLock() defer c.lock.RUnlock() for i, sub := range c.subs { ss[i] = sub.String() } return strings.Join(ss, " Then: ") } var _ Interface = new(chain) // Chain(n1, cond1, n2, cond2, ...) returns cond1.OK() the first n1 // times OK() is called, cond2.OK() the following n2 times, etc. func Chain(args ...interface{}) *chain { iters := make([]*_iter, 0, len(args)/2) for len(args) > 1 { rem := args[0].(int) sub := args[1].(Interface) iters = append(iters, &_iter{sub, rem}) args = args[2:] } return &chain{subs: iters} } ubuntu-push-0.68+16.04.20160310.2/testing/condition/condition_test.go0000644000015600001650000000521012670364255025472 0ustar pbuserpbgroup00000000000000/* 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 condition import ( "testing" "launchpad.net/gocheck" // not into . because we have our own Not ) // hook up gocheck func Test(t *testing.T) { gocheck.TestingT(t) } type CondSuite struct{} var _ = gocheck.Suite(&CondSuite{}) func (s *CondSuite) TestConditionWorkTrue(c *gocheck.C) { cond := Work(true) c.Check(cond.OK(), gocheck.Equals, true) c.Check(cond.String(), gocheck.Equals, "Always Working.") } func (s *CondSuite) TestConditionWorkFalse(c *gocheck.C) { cond := Work(false) c.Check(cond.OK(), gocheck.Equals, false) c.Check(cond.String(), gocheck.Equals, "Never Working.") } func (s *CondSuite) TestConditionFail2Work(c *gocheck.C) { cond := Fail2Work(2) c.Check(cond.String(), gocheck.Equals, "Still Broken, 2 to go.") c.Check(cond.OK(), gocheck.Equals, false) c.Check(cond.String(), gocheck.Equals, "Still Broken, 1 to go.") c.Check(cond.OK(), gocheck.Equals, false) c.Check(cond.String(), gocheck.Equals, "Working.") c.Check(cond.OK(), gocheck.Equals, true) c.Check(cond.String(), gocheck.Equals, "Working.") c.Check(cond.OK(), gocheck.Equals, true) c.Check(cond.String(), gocheck.Equals, "Working.") } func (s *CondSuite) TestConditionNot(c *gocheck.C) { cond := Not(Fail2Work(1)) c.Check(cond.String(), gocheck.Equals, "Not Still Broken, 1 to go.") c.Check(cond.OK(), gocheck.Equals, true) c.Check(cond.String(), gocheck.Equals, "Not Working.") c.Check(cond.OK(), gocheck.Equals, false) } func (s *CondSuite) TestConditionChain(c *gocheck.C) { cond := Chain(2, Work(true), 3, Work(false), 0, Work(true)) c.Check(cond.String(), gocheck.Equals, "2 of Always Working. Then: 3 of Never Working. Then: 0 of Always Working.") c.Check(cond.OK(), gocheck.Equals, true) c.Check(cond.OK(), gocheck.Equals, true) c.Check(cond.OK(), gocheck.Equals, false) c.Check(cond.OK(), gocheck.Equals, false) c.Check(cond.OK(), gocheck.Equals, false) c.Check(cond.OK(), gocheck.Equals, true) // c.Check(cond.OK(), gocheck.Equals, true) // c.Check(cond.OK(), gocheck.Equals, true) // c.Check(cond.OK(), gocheck.Equals, true) } ubuntu-push-0.68+16.04.20160310.2/testing/helpers.go0000644000015600001650000001055412670364255022130 0ustar pbuserpbgroup00000000000000/* 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 helpers for testing. package testing import ( "encoding/json" "fmt" "net/url" "os" "path" "path/filepath" "runtime" "strings" "sync" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/protocol" ) type captureHelper struct { outputFunc func(int, string) error lock sync.Mutex logEvents []string logEventCb func(string) } func (h *captureHelper) Output(calldepth int, s string) error { err := h.outputFunc(calldepth+2, s) if err == nil { h.lock.Lock() defer h.lock.Unlock() if h.logEventCb != nil { h.logEventCb(s) } h.logEvents = append(h.logEvents, s+"\n") } return err } func (h *captureHelper) captured() string { h.lock.Lock() defer h.lock.Unlock() return strings.Join(h.logEvents, "") } func (h *captureHelper) reset() { h.lock.Lock() defer h.lock.Unlock() h.logEvents = nil } func (h *captureHelper) setLogEventCb(cb func(string)) { h.lock.Lock() defer h.lock.Unlock() h.logEventCb = cb } // TestLogger implements logger.Logger using gocheck.C and supporting // capturing log strings. type TestLogger struct { logger.Logger helper *captureHelper } // NewTestLogger can be used in tests instead of // NewSimpleLogger(FromMinimalLogger). func NewTestLogger(minLog logger.MinimalLogger, level string) *TestLogger { h := &captureHelper{outputFunc: minLog.Output} log := &TestLogger{ Logger: logger.NewSimpleLoggerFromMinimalLogger(h, level), helper: h, } return log } // Captured returns accumulated log events. func (tlog *TestLogger) Captured() string { return tlog.helper.captured() } // Reset resets accumulated log events. func (tlog *TestLogger) ResetCapture() { tlog.helper.reset() } // SetLogEventCb sets a callback invoked for log events. func (tlog *TestLogger) SetLogEventCb(cb func(string)) { tlog.helper.setLogEventCb(cb) } // SourceRelative produces a path relative to the source code, makes // sense only for tests when the code is available on disk. func SourceRelative(relativePath string) string { _, file, _, ok := runtime.Caller(1) if !ok { panic("failed to get source filename using Caller()") } dir := filepath.Dir(file) root := os.Getenv("UBUNTU_PUSH_TEST_RESOURCES_ROOT") if root != "" { const sep = "launchpad.net/ubuntu-push/" idx := strings.LastIndex(dir, sep) if idx == -1 { panic(fmt.Errorf("unable to find %s in %#v", sep, dir)) } idx += len(sep) dir = filepath.Join(root, dir[idx:]) } return filepath.Join(dir, relativePath) } // ScriptAbsPath gets the absolute path to a script in the scripts directory // assuming we're in a subdirectory of the project func ScriptAbsPath(script string) string { cwd, err := os.Getwd() if err != nil { panic(fmt.Errorf("unable to get working directory: %v", err)) } if !path.IsAbs(cwd) { panic(fmt.Errorf("working directory not absolute? %v", cwd)) } for cwd != "/" { filename := path.Join(cwd, "scripts", script) _, err := os.Stat(filename) if err == nil { return filename } if !os.IsNotExist(err) { panic(fmt.Errorf("unable to stat %v: %v", filename, err)) } cwd = path.Dir(cwd) } panic(fmt.Errorf("unable to find script %v", script)) } // Ns makes a []Notification from just payloads. func Ns(payloads ...json.RawMessage) []protocol.Notification { res := make([]protocol.Notification, len(payloads)) for i := 0; i < len(payloads); i++ { res[i].Payload = payloads[i] } return res } // ParseURL parses a URL conveniently. func ParseURL(s string) *url.URL { purl, err := url.Parse(s) if err != nil { panic(err) } return purl } // DumpGoroutines dumps current goroutines. func DumpGoroutines() { var buf [64 * 1024]byte sz := runtime.Stack(buf[:], true) dump := string(buf[:sz]) fmt.Println(dump) fmt.Println("#goroutines#", strings.Count("\n"+dump, "\ngoroutine ")) } ubuntu-push-0.68+16.04.20160310.2/LICENSE0000644000015600001650000000116512670364255017465 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2015 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 . */ ubuntu-push-0.68+16.04.20160310.2/identifier/0000755000015600001650000000000012670364532020575 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/identifier/testing/0000755000015600001650000000000012670364532022252 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/identifier/testing/testing_test.go0000644000015600001650000000323712670364255025324 0ustar pbuserpbgroup00000000000000/* 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 import ( "testing" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/identifier" ) // hook up gocheck func Test(t *testing.T) { TestingT(t) } type IdentifierSuite struct{} var _ = Suite(&IdentifierSuite{}) // TestSettableDefaultValueVisible tests that SettableIdentifier's default // value is notable. func (s *IdentifierSuite) TestSettableDefaultValueVisible(c *C) { id := Settable() c.Check(id.String(), Equals, "") } // TestSettableSets tests that SettableIdentifier is settable. func (s *IdentifierSuite) TestSettableSets(c *C) { id := Settable() id.Set("hello") c.Check(id.String(), Equals, "hello") } // TestFailingStringNotEmpty tests that FailingIdentifier still has a // non-empty string. func (s *IdentifierSuite) TestFailingStringNotEmpty(c *C) { id := Failing() c.Check(id.String(), Equals, "") } // TestIdentifierInterface tests that FailingIdentifier and // SettableIdentifier implement Id. func (s *IdentifierSuite) TestIdentifierInterface(c *C) { _ = []identifier.Id{Failing(), Settable()} } ubuntu-push-0.68+16.04.20160310.2/identifier/testing/testing.go0000644000015600001650000000332512670364255024263 0ustar pbuserpbgroup00000000000000/* 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 implements a couple of Ids that are useful // for testing things that use identifier. package testing // SettableIdentifier is an Id that lets you set the value of the identifier. // // By default the identifier's value is "", so it's visible // if you're misusing it. type SettableIdentifier struct { value string } // Settable is the constructor for SettableIdentifier. func Settable() *SettableIdentifier { return &SettableIdentifier{""} } // Set is the method you use to set the identifier. func (sid *SettableIdentifier) Set(value string) { sid.value = value } // String returns the string you set. func (sid *SettableIdentifier) String() string { return sid.value } // FailingIdentifier is an Id that always fails to generate. type FailingIdentifier struct{} // Failing is the constructor for FailingIdentifier. func Failing() *FailingIdentifier { return &FailingIdentifier{} } // String returns "". // // The purpose of this is to make it easy to spot if you're using it // by accident. func (*FailingIdentifier) String() string { return "" } ubuntu-push-0.68+16.04.20160310.2/identifier/identifier.go0000644000015600001650000000307512670364255023255 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ // Package identifier is the source of an anonymous and stable // system id (from /var/lib/dbus/machine-id) used by the Ubuntu // push notifications service. package identifier import ( "fmt" "io/ioutil" ) var machineIdPath = "/var/lib/dbus/machine-id" // an Id knows how to generate itself, and how to stringify itself. type Id interface { String() string } // Identifier is the default Id implementation. type Identifier struct { value string } func readMachineId() (string, error) { value, err := ioutil.ReadFile(machineIdPath) if err != nil { return "", err } return string(value)[:len(value)-1], nil } // New creates an Identifier func New() (Id, error) { value, err := readMachineId() if err != nil { return &Identifier{value: ""}, fmt.Errorf("failed to read the machine id: %s", err) } return &Identifier{value: value}, nil } // String returns the system identifier as a string. func (id *Identifier) String() string { return id.value } ubuntu-push-0.68+16.04.20160310.2/identifier/identifier_test.go0000644000015600001650000000323512670364255024312 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package identifier import ( . "launchpad.net/gocheck" "os" "testing" ) // hook up gocheck func Test(t *testing.T) { TestingT(t) } type IdentifierSuite struct{} var _ = Suite(&IdentifierSuite{}) // TestNew checks that New does not fail, and returns a // 32-byte string. func (s *IdentifierSuite) TestNew(c *C) { _, err := os.Stat(machineIdPath) if os.IsNotExist(err) { c.Skip("no dbus machine id") } id, err := New() c.Check(err, IsNil) c.Check(id.String(), HasLen, 32) } // TestNewFail checks that when we can't read the machine-id // file the error is propagated func (s *IdentifierSuite) TestNewFail(c *C) { // replace the machine-id file path machineIdPath = "/var/lib/dbus/no-such-file" id, err := New() c.Check(err, NotNil) c.Check(err.Error(), Equals, "failed to read the machine id: open /var/lib/dbus/no-such-file: no such file or directory") c.Check(id.String(), HasLen, 0) } // TestIdentifierInterface checks that Identifier implements Id. func (s *IdentifierSuite) TestIdentifierInterface(c *C) { id, _ := New() _ = []Id{id} } ubuntu-push-0.68+16.04.20160310.2/messaging/0000755000015600001650000000000012670364532020430 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/messaging/reply/0000755000015600001650000000000012670364532021563 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/messaging/reply/reply.go0000644000015600001650000000170412670364255023251 0ustar pbuserpbgroup00000000000000/* 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 . */ // reply is where we keep MMActionReply, so we can use it from // messaging and cmessaging without going circular about it package reply import "launchpad.net/ubuntu-push/click" // MMActionReply holds the reply from a MessagingMenu action type MMActionReply struct { Notification string Action string App *click.AppId } ubuntu-push-0.68+16.04.20160310.2/messaging/messaging_test.go0000644000015600001650000003163112670364255024001 0ustar pbuserpbgroup00000000000000/* 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 messaging import ( "sort" "strconv" "sync" "time" . "launchpad.net/gocheck" "testing" "launchpad.net/ubuntu-push/click" clickhelp "launchpad.net/ubuntu-push/click/testing" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/messaging/cmessaging" "launchpad.net/ubuntu-push/messaging/reply" helpers "launchpad.net/ubuntu-push/testing" ) // hook up gocheck func Test(t *testing.T) { TestingT(t) } type MessagingSuite struct { log *helpers.TestLogger app *click.AppId } var _ = Suite(&MessagingSuite{}) func (ms *MessagingSuite) SetUpSuite(c *C) { cAddNotification = func(a string, n string, c *launch_helper.Card, payload *cmessaging.Payload) { ms.log.Debugf("ADD: app: %s, not: %s, card: %v, chan: %v", a, n, c, payload) } cRemoveNotification = func(a, n string) { ms.log.Debugf("REMOVE: app: %s, not: %s", a, n) } } func (ms *MessagingSuite) TearDownSuite(c *C) { cAddNotification = cmessaging.AddNotification cRemoveNotification = cmessaging.RemoveNotification cNotificationExists = cmessaging.NotificationExists } func (ms *MessagingSuite) SetUpTest(c *C) { ms.log = helpers.NewTestLogger(c, "debug") ms.app = clickhelp.MustParseAppId("com.example.test_test_0") // just in case cNotificationExists = nil } func (ms *MessagingSuite) TestPresentPresents(c *C) { mmu := New(ms.log) card := launch_helper.Card{Summary: "ehlo", Persist: true} notif := launch_helper.Notification{Card: &card} c.Check(mmu.Present(ms.app, "notif-id", ¬if), Equals, true) c.Check(ms.log.Captured(), Matches, `(?s).* ADD:.*notif-id.*`) } func (ms *MessagingSuite) TestPresentDoesNotPresentsIfNoSummary(c *C) { mmu := New(ms.log) card := launch_helper.Card{Persist: true} notif := launch_helper.Notification{Card: &card} c.Check(mmu.Present(ms.app, "notif-id", ¬if), Equals, false) c.Check(ms.log.Captured(), Matches, "(?sm).*has no persistable card.*") } func (ms *MessagingSuite) TestPresentDoesNotPresentsIfNotPersist(c *C) { mmu := New(ms.log) card := launch_helper.Card{Summary: "ehlo"} notif := launch_helper.Notification{Card: &card} c.Check(mmu.Present(ms.app, "notif-id", ¬if), Equals, false) c.Check(ms.log.Captured(), Matches, "(?sm).*has no persistable card.*") } func (ms *MessagingSuite) TestPresentPanicsIfNil(c *C) { mmu := New(ms.log) c.Check(func() { mmu.Present(ms.app, "notif-id", nil) }, Panics, `please check notification is not nil before calling present`) } func (ms *MessagingSuite) TestPresentDoesNotPresentsIfNilCard(c *C) { mmu := New(ms.log) c.Check(mmu.Present(ms.app, "notif-id", &launch_helper.Notification{}), Equals, false) c.Check(ms.log.Captured(), Matches, "(?sm).*no persistable card.*") } func (ms *MessagingSuite) TestPresentWithActions(c *C) { mmu := New(ms.log) card := launch_helper.Card{Summary: "ehlo", Persist: true, Actions: []string{"action-1"}} notif := launch_helper.Notification{Card: &card, Tag: "a-tag"} c.Check(mmu.Present(ms.app, "notif-id", ¬if), Equals, true) c.Check(ms.log.Captured(), Matches, `(?s).* ADD:.*notif-id.*`) payload, _ := mmu.notifications["notif-id"] c.Check(payload.Ch, Equals, mmu.Ch) c.Check(len(payload.Actions), Equals, 2) c.Check(payload.Tag, Equals, "a-tag") rawAction := "{\"app\":\"com.example.test_test_0\",\"act\":\"action-1\",\"nid\":\"notif-id\"}" c.Check(payload.Actions[0], Equals, rawAction) c.Check(payload.Actions[1], Equals, "action-1") } func (msg *MessagingSuite) checkTags(c *C, got, expected []string) { sort.Strings(got) sort.Strings(expected) c.Check(got, DeepEquals, expected) } func (ms *MessagingSuite) TestTagsListsTags(c *C) { mmu := New(ms.log) f := func(s string) *launch_helper.Notification { card := launch_helper.Card{Summary: "tag: \"" + s + "\"", Persist: true} return &launch_helper.Notification{Card: &card, Tag: s} } existsCount := 0 // patch cNotificationExists to return true cNotificationExists = func(did string, nid string) bool { existsCount++ return true } c.Check(mmu.Tags(ms.app), IsNil) c.Assert(mmu.Present(ms.app, "notif1", f("one")), Equals, true) ms.checkTags(c, mmu.Tags(ms.app), []string{"one"}) c.Check(existsCount, Equals, 1) existsCount = 0 c.Assert(mmu.Present(ms.app, "notif2", f("")), Equals, true) ms.checkTags(c, mmu.Tags(ms.app), []string{"one", ""}) c.Check(existsCount, Equals, 2) // and an empty notification doesn't count c.Assert(mmu.Present(ms.app, "notif3", &launch_helper.Notification{Tag: "X"}), Equals, false) ms.checkTags(c, mmu.Tags(ms.app), []string{"one", ""}) // and they go away if we remove one mmu.RemoveNotification("notif1", false) ms.checkTags(c, mmu.Tags(ms.app), []string{""}) mmu.RemoveNotification("notif2", false) c.Check(mmu.Tags(ms.app), IsNil) } func (ms *MessagingSuite) TestClearClears(c *C) { app1 := ms.app app2 := clickhelp.MustParseAppId("com.example.test_test-2_0") app3 := clickhelp.MustParseAppId("com.example.test_test-3_0") mm := New(ms.log) f := func(app *click.AppId, nid string, tag string, withCard bool) bool { notif := launch_helper.Notification{Tag: tag} card := launch_helper.Card{Summary: "tag: \"" + tag + "\"", Persist: true} if withCard { notif.Card = &card } return mm.Present(app, nid, ¬if) } // create a bunch c.Assert(f(app1, "notif1", "one", true), Equals, true) c.Assert(f(app1, "notif2", "two", true), Equals, true) c.Assert(f(app1, "notif3", "", true), Equals, true) c.Assert(f(app2, "notif4", "one", true), Equals, true) c.Assert(f(app2, "notif5", "two", true), Equals, true) c.Assert(f(app3, "notif6", "one", true), Equals, true) c.Assert(f(app3, "notif7", "", true), Equals, true) // patch cNotificationExists to return true in order to make sure that messages // do not get deleted by the doCleanUpTags() call in the Tags() function cNotificationExists = func(did string, nid string) bool { return true } // that is: // app 1: "one", "two", ""; // app 2: "one", "two"; // app 3: "one", "" ms.checkTags(c, mm.Tags(app1), []string{"one", "two", ""}) ms.checkTags(c, mm.Tags(app2), []string{"one", "two"}) ms.checkTags(c, mm.Tags(app3), []string{"one", ""}) // clearing a non-existent tag does nothing c.Check(mm.Clear(app1, "foo"), Equals, 0) c.Check(mm.Tags(app1), HasLen, 3) c.Check(mm.Tags(app2), HasLen, 2) c.Check(mm.Tags(app3), HasLen, 2) // asking to clear a tag that exists only for another app does nothing c.Check(mm.Clear(app3, "two"), Equals, 0) c.Check(mm.Tags(app1), HasLen, 3) c.Check(mm.Tags(app2), HasLen, 2) c.Check(mm.Tags(app3), HasLen, 2) // asking to clear a list of tags, only one of which is yours, only clears yours c.Check(mm.Clear(app3, "one", "two"), Equals, 1) c.Check(mm.Tags(app1), HasLen, 3) c.Check(mm.Tags(app2), HasLen, 2) c.Check(mm.Tags(app3), HasLen, 1) // clearing with no args just empties it c.Check(mm.Clear(app1), Equals, 3) c.Check(mm.Tags(app1), IsNil) c.Check(mm.Tags(app2), HasLen, 2) c.Check(mm.Tags(app3), HasLen, 1) // asking to clear all the tags from an already tagless app does nothing c.Check(mm.Clear(app1), Equals, 0) c.Check(mm.Tags(app1), IsNil) c.Check(mm.Tags(app2), HasLen, 2) c.Check(mm.Tags(app3), HasLen, 1) // check we work ok with a "" tag, too. c.Check(mm.Clear(app1, ""), Equals, 0) c.Check(mm.Clear(app2, ""), Equals, 0) c.Check(mm.Clear(app3, ""), Equals, 1) c.Check(mm.Tags(app1), IsNil) c.Check(mm.Tags(app2), HasLen, 2) c.Check(mm.Tags(app3), HasLen, 0) } func (ms *MessagingSuite) TestRemoveNotification(c *C) { mmu := New(ms.log) card := launch_helper.Card{Summary: "ehlo", Persist: true, Actions: []string{"action-1"}} actions := []string{"{\"app\":\"com.example.test_test_0\",\"act\":\"action-1\",\"nid\":\"notif-id\"}", "action-1"} mmu.addNotification(ms.app, "notif-id", "a-tag", &card, actions, nil) // check it's there payload, ok := mmu.notifications["notif-id"] c.Check(ok, Equals, true) c.Check(payload.Actions, DeepEquals, actions) c.Check(payload.Tag, Equals, "a-tag") c.Check(payload.Ch, Equals, mmu.Ch) // remove the notification (internal only) mmu.RemoveNotification("notif-id", false) // check it's gone _, ok = mmu.notifications["notif-id"] c.Check(ok, Equals, false) } func (ms *MessagingSuite) TestRemoveNotificationsFromUI(c *C) { mmu := New(ms.log) card := launch_helper.Card{Summary: "ehlo", Persist: true, Actions: []string{"action-1"}} actions := []string{"{\"app\":\"com.example.test_test_0\",\"act\":\"action-1\",\"nid\":\"notif-id\"}", "action-1"} mmu.addNotification(ms.app, "notif-id", "a-tag", &card, actions, nil) // check it's there _, ok := mmu.notifications["notif-id"] c.Assert(ok, Equals, true) // remove the notification (both internal and from UI) mmu.RemoveNotification("notif-id", true) // check it's gone _, ok = mmu.notifications["notif-id"] c.Check(ok, Equals, false) // and check it's been removed from the UI too c.Check(ms.log.Captured(), Matches, `(?s).* REMOVE:.*notif-id.*`) } func (ms *MessagingSuite) TestCleanupStaleNotification(c *C) { mmu := New(ms.log) card := launch_helper.Card{Summary: "ehlo", Persist: true, Actions: []string{"action-1"}} actions := []string{"{\"app\":\"com.example.test_test_0\",\"act\":\"action-1\",\"nid\":\"notif-id\"}", "action-1"} mmu.addNotification(ms.app, "notif-id", "", &card, actions, nil) // check it's there _, ok := mmu.notifications["notif-id"] c.Check(ok, Equals, true) // patch cNotificationExists to return true cNotificationExists = func(did string, nid string) bool { return true } // remove the notification mmu.cleanUpNotifications() // check it's still there _, ok = mmu.notifications["notif-id"] c.Check(ok, Equals, true) // patch cNotificationExists to return false cNotificationExists = func(did string, nid string) bool { return false } // remove the notification mmu.cleanUpNotifications() // check it's gone _, ok = mmu.notifications["notif-id"] c.Check(ok, Equals, false) } func (ms *MessagingSuite) TestCleanupInAddNotification(c *C) { mmu := New(ms.log) var wg sync.WaitGroup var cleanUpAsynchronously = func() { wg.Add(1) go func() { defer wg.Done() mmu.cleanUpNotifications() }() } showNotification := func(number int) { action := "action-" + strconv.Itoa(number) notificationId := "notif-id-" + strconv.Itoa(number) card := launch_helper.Card{Summary: "ehlo", Persist: true, Actions: []string{action}} actions := []string{"{\"app\":\"com.example.test_test_0\",\"act\":\"" + action + "\",\"nid\":\"" + notificationId + "\"}", action} mmu.addNotification(ms.app, notificationId, "", &card, actions, cleanUpAsynchronously) } // Add 20 notifications for i := 0; i < 20; i++ { showNotification(i) } // wait for the cleanup goroutine in addNotification to finish in case it gets called (which it shouldn't!) wg.Wait() // check that we have got 20 notifications c.Check(mmu.notifications, HasLen, 20) // patch cNotificationExists to return true cNotificationExists = func(did string, nid string) bool { return true } // adding another notification should not remove the current ones showNotification(21) // wait for the cleanup goroutine in addNotification to finish in case it gets called (which it shouldn't!) wg.Wait() // check we that have 21 notifications now c.Check(mmu.notifications, HasLen, 21) // patch cNotificationExists to return false for all but the next one we are going to add cNotificationExists = func(did string, nid string) bool { return nid == "notif-id-22" } // adding another notification should not remove the current ones as mmu.lastCleanupTime is too recent showNotification(22) // wait for the cleanup goroutine in addNotification to finish in case it gets called (which it shouldn't!) wg.Wait() // check we that have got 22 notifications now c.Check(mmu.notifications, HasLen, 22) // set back the lastCleanupTime to 11 minutes ago mmu.lastCleanupTime = mmu.lastCleanupTime.Add(-11 * time.Minute) // patch cNotificationExists to return false for all but the next one we are going to add cNotificationExists = func(did string, nid string) bool { return nid == "notif-id-23" } // adding another notification should remove all previous ones now showNotification(23) // wait for the cleanup goroutine in addNotification to finish wg.Wait() // check that all notifications except the last one have been removed c.Check(mmu.notifications, HasLen, 1) } func (ms *MessagingSuite) TestGetCh(c *C) { mmu := New(ms.log) mmu.Ch = make(chan *reply.MMActionReply) c.Check(mmu.GetCh(), Equals, mmu.Ch) } ubuntu-push-0.68+16.04.20160310.2/messaging/cmessaging/0000755000015600001650000000000012670364532022550 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/messaging/cmessaging/cmessaging_c.go0000644000015600001650000000642512670364255025532 0ustar pbuserpbgroup00000000000000/* 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 /* #include #include // this is a .go file instead of a .c file because of dh-golang limitations void handleActivate(gchar* c_action, const gchar * c_notification , gpointer obj); static void activate_cb(MessagingMenuMessage* msg, gchar* action, GVariant* parameter, gpointer obj) { handleActivate(action, messaging_menu_message_get_id(msg), obj); } static GHashTable* map = NULL; void add_notification (const gchar* desktop_id, const gchar* notification_id, const gchar* icon_path, const gchar* summary, const gchar* body, gint64 timestamp, const gchar** actions, gpointer obj) { if (map == NULL) { map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); } MessagingMenuApp* app = g_hash_table_lookup (map, desktop_id); if (app == NULL) { GIcon* app_icon = g_icon_new_for_string (desktop_id, NULL); app = messaging_menu_app_new (desktop_id); messaging_menu_app_register (app); messaging_menu_app_append_source (app, "postal", app_icon, "Postal"); g_hash_table_insert (map, g_strdup (desktop_id), app); g_object_unref (app_icon); // app only has a single refcount, and it's stored in the map. No need to g_object_unref it! } GIcon* icon = g_icon_new_for_string(icon_path, NULL); MessagingMenuMessage* msg = messaging_menu_message_new(notification_id, icon, summary, "", body, timestamp); // unity8 support for actions in the messaging menu is strange. Not doing that for now. messaging_menu_app_append_message(app, msg, "postal", TRUE); g_signal_connect(msg, "activate", G_CALLBACK(activate_cb), obj); g_object_unref(msg); } void remove_notification (const gchar* desktop_id, const gchar* notification_id) { if (map == NULL) { return; } MessagingMenuApp* app = g_hash_table_lookup (map, desktop_id); if (app == NULL) { // no app in the hash table, bailout return; } messaging_menu_app_remove_message_by_id (app, notification_id); } gboolean notification_exists (const gchar* desktop_id, const gchar* notification_id) { if (map == NULL) { return FALSE; } MessagingMenuApp* app = g_hash_table_lookup (map, desktop_id); if (app == NULL) { // no app in the hash table, bailout return FALSE; } MessagingMenuMessage* msg = messaging_menu_app_get_message(app, notification_id); if (msg != NULL) { // the notification is still there return TRUE; } return FALSE; } */ import "C" ubuntu-push-0.68+16.04.20160310.2/messaging/cmessaging/cmessaging.go0000644000015600001650000000616712670364255025233 0ustar pbuserpbgroup00000000000000/* 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 cmessaging /* #cgo pkg-config: messaging-menu #include void add_notification(const gchar* desktop_id, const gchar* notification_id, const gchar* icon_path, const gchar* summary, const gchar* body, gint64 timestamp, const gchar** actions, gpointer obj); void remove_notification(const gchar* desktop_id, const gchar* notification_id); gboolean notification_exists(const gchar* desktop_id, const gchar* notification_id); */ import "C" import "unsafe" import ( "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/messaging/reply" ) type Payload struct { Ch chan *reply.MMActionReply Actions []string App *click.AppId Tag string } func gchar(s string) *C.gchar { return (*C.gchar)(C.CString(s)) } func gfree(s *C.gchar) { C.g_free((C.gpointer)(s)) } //export handleActivate func handleActivate(c_action *C.char, c_notification *C.char, obj unsafe.Pointer) { payload := (*Payload)(obj) action := C.GoString(c_action) // Default action, only support ATM, is always "". // Use the first action as the default if it's available. if action == "" && len(payload.Actions) >= 2 { action = payload.Actions[1] } mmar := &reply.MMActionReply{Notification: C.GoString(c_notification), Action: action, App: payload.App} payload.Ch <- mmar } func AddNotification(desktopId string, notificationId string, card *launch_helper.Card, payload *Payload) { desktop_id := gchar(desktopId) defer gfree(desktop_id) notification_id := gchar(notificationId) defer gfree(notification_id) icon_path := gchar(card.Icon) defer gfree(icon_path) summary := gchar(card.Summary) defer gfree(summary) body := gchar(card.Body) defer gfree(body) timestamp := (C.gint64)(card.Timestamp() * 1000000) C.add_notification(desktop_id, notification_id, icon_path, summary, body, timestamp, nil, (C.gpointer)(payload)) } func RemoveNotification(desktopId string, notificationId string) { desktop_id := gchar(desktopId) defer gfree(desktop_id) notification_id := gchar(notificationId) defer gfree(notification_id) C.remove_notification(desktop_id, notification_id) } func NotificationExists(desktopId string, notificationId string) bool { notification_id := gchar(notificationId) defer gfree(notification_id) desktop_id := gchar(desktopId) defer gfree(desktop_id) return C.notification_exists(desktop_id, notification_id) == C.TRUE } func init() { go C.g_main_loop_run(C.g_main_loop_new(nil, C.FALSE)) } ubuntu-push-0.68+16.04.20160310.2/messaging/messaging.go0000644000015600001650000001231012670364255022733 0ustar pbuserpbgroup00000000000000/* 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 messaging wraps the messaging menu indicator, allowing for persistent // notifications to the user. package messaging import ( "encoding/json" "sync" "time" "launchpad.net/ubuntu-push/bus/notifications" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/messaging/cmessaging" "launchpad.net/ubuntu-push/messaging/reply" ) type MessagingMenu struct { Log logger.Logger Ch chan *reply.MMActionReply notifications map[string]*cmessaging.Payload // keep a ref to the Payload used in the MMU callback lock sync.RWMutex lastCleanupTime time.Time } type cleanUp func() // New returns a new MessagingMenu func New(log logger.Logger) *MessagingMenu { return &MessagingMenu{Log: log, Ch: make(chan *reply.MMActionReply), notifications: make(map[string]*cmessaging.Payload)} } var cAddNotification = cmessaging.AddNotification var cRemoveNotification = cmessaging.RemoveNotification var cNotificationExists = cmessaging.NotificationExists // GetCh returns the reply channel, exactly like mm.Ch. func (mmu *MessagingMenu) GetCh() chan *reply.MMActionReply { return mmu.Ch } func (mmu *MessagingMenu) addNotification(app *click.AppId, notificationId string, tag string, card *launch_helper.Card, actions []string, testingCleanUpFunction cleanUp) { mmu.lock.Lock() defer mmu.lock.Unlock() payload := &cmessaging.Payload{Ch: mmu.Ch, Actions: actions, App: app, Tag: tag} mmu.notifications[notificationId] = payload cAddNotification(app.DesktopId(), notificationId, card, payload) // Clean up our internal notifications store if it holds more than 20 messages (and apparently nobody ever calls Tags()) if len(mmu.notifications) > 20 && time.Since(mmu.lastCleanupTime).Minutes() > 10 { mmu.lastCleanupTime = time.Now() if testingCleanUpFunction == nil { go mmu.cleanUpNotifications() } else { testingCleanUpFunction() // Has to implement the asynchronous part itself } } } func (mmu *MessagingMenu) RemoveNotification(notificationId string, fromUI bool) { mmu.lock.Lock() defer mmu.lock.Unlock() payload := mmu.notifications[notificationId] delete(mmu.notifications, notificationId) if payload != nil && payload.App != nil && fromUI { cRemoveNotification(payload.App.DesktopId(), notificationId) } } func (mmu *MessagingMenu) cleanUpNotifications() { mmu.lock.Lock() defer mmu.lock.Unlock() mmu.doCleanUpNotifications() } // doCleanupNotifications removes notifications that were cleared from the messaging menu func (mmu *MessagingMenu) doCleanUpNotifications() { for nid, payload := range mmu.notifications { if !cNotificationExists(payload.App.DesktopId(), nid) { delete(mmu.notifications, nid) } } } func (mmu *MessagingMenu) Tags(app *click.AppId) []string { orig := app.Original() tags := []string(nil) mmu.lock.Lock() defer mmu.lock.Unlock() mmu.lastCleanupTime = time.Now() mmu.doCleanUpNotifications() for _, payload := range mmu.notifications { if payload.App.Original() == orig { tags = append(tags, payload.Tag) } } return tags } func (mmu *MessagingMenu) Clear(app *click.AppId, tags ...string) int { orig := app.Original() var nids []string mmu.lock.RLock() // O(n×m). Should be small n and m though. for nid, payload := range mmu.notifications { if payload.App.Original() == orig { if len(tags) == 0 { nids = append(nids, nid) } else { for _, tag := range tags { if payload.Tag == tag { nids = append(nids, nid) } } } } } mmu.lock.RUnlock() for _, nid := range nids { mmu.RemoveNotification(nid, true) } mmu.cleanUpNotifications() return len(nids) } func (mmu *MessagingMenu) Present(app *click.AppId, nid string, notification *launch_helper.Notification) bool { if notification == nil { panic("please check notification is not nil before calling present") } card := notification.Card if card == nil || !card.Persist || card.Summary == "" { mmu.Log.Debugf("[%s] notification has no persistable card: %#v", nid, card) return false } actions := make([]string, 2*len(card.Actions)) for i, action := range card.Actions { act, err := json.Marshal(¬ifications.RawAction{ App: app, Nid: nid, ActionId: i, Action: action, }) if err != nil { mmu.Log.Errorf("failed to build action: %s", action) return false } actions[2*i] = string(act) actions[2*i+1] = action } mmu.Log.Debugf("[%s] creating notification centre entry for %s (summary: %s)", nid, app.Base(), card.Summary) mmu.addNotification(app, nid, notification.Tag, card, actions, nil) return true } ubuntu-push-0.68+16.04.20160310.2/external/0000755000015600001650000000000012670364532020275 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/external/murmur3/0000755000015600001650000000000012670364532021707 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/external/murmur3/murmur128.go0000644000015600001650000000675612670364255024040 0ustar pbuserpbgroup00000000000000package murmur3 import ( //"encoding/binary" "hash" "unsafe" ) const ( c1_128 = 0x87c37b91114253d5 c2_128 = 0x4cf5ad432745937f ) // Make sure interfaces are correctly implemented. var ( _ hash.Hash = new(digest128) _ Hash128 = new(digest128) _ bmixer = new(digest128) ) // Hack: the standard api doesn't define any Hash128 interface. type Hash128 interface { hash.Hash Sum128() (uint64, uint64) } // digest128 represents a partial evaluation of a 128 bites hash. type digest128 struct { digest h1 uint64 // Unfinalized running hash part 1. h2 uint64 // Unfinalized running hash part 2. } func New128() Hash128 { d := new(digest128) d.bmixer = d d.Reset() return d } func (d *digest128) Size() int { return 16 } func (d *digest128) reset() { d.h1, d.h2 = 0, 0 } func (d *digest128) Sum(b []byte) []byte { h1, h2 := d.h1, d.h2 return append(b, byte(h1>>56), byte(h1>>48), byte(h1>>40), byte(h1>>32), byte(h1>>24), byte(h1>>16), byte(h1>>8), byte(h1), byte(h2>>56), byte(h2>>48), byte(h2>>40), byte(h2>>32), byte(h2>>24), byte(h2>>16), byte(h2>>8), byte(h2), ) } func (d *digest128) bmix(p []byte) (tail []byte) { h1, h2 := d.h1, d.h2 nblocks := len(p) / 16 for i := 0; i < nblocks; i++ { t := (*[2]uint64)(unsafe.Pointer(&p[i*16])) k1, k2 := t[0], t[1] k1 *= c1_128 k1 = (k1 << 31) | (k1 >> 33) // rotl64(k1, 31) k1 *= c2_128 h1 ^= k1 h1 = (h1 << 27) | (h1 >> 37) // rotl64(h1, 27) h1 += h2 h1 = h1*5 + 0x52dce729 k2 *= c2_128 k2 = (k2 << 33) | (k2 >> 31) // rotl64(k2, 33) k2 *= c1_128 h2 ^= k2 h2 = (h2 << 31) | (h2 >> 33) // rotl64(h2, 31) h2 += h1 h2 = h2*5 + 0x38495ab5 } d.h1, d.h2 = h1, h2 return p[nblocks*d.Size():] } func (d *digest128) Sum128() (h1, h2 uint64) { h1, h2 = d.h1, d.h2 var k1, k2 uint64 switch len(d.tail) & 15 { case 15: k2 ^= uint64(d.tail[14]) << 48 fallthrough case 14: k2 ^= uint64(d.tail[13]) << 40 fallthrough case 13: k2 ^= uint64(d.tail[12]) << 32 fallthrough case 12: k2 ^= uint64(d.tail[11]) << 24 fallthrough case 11: k2 ^= uint64(d.tail[10]) << 16 fallthrough case 10: k2 ^= uint64(d.tail[9]) << 8 fallthrough case 9: k2 ^= uint64(d.tail[8]) << 0 k2 *= c2_128 k2 = (k2 << 33) | (k2 >> 31) // rotl64(k2, 33) k2 *= c1_128 h2 ^= k2 fallthrough case 8: k1 ^= uint64(d.tail[7]) << 56 fallthrough case 7: k1 ^= uint64(d.tail[6]) << 48 fallthrough case 6: k1 ^= uint64(d.tail[5]) << 40 fallthrough case 5: k1 ^= uint64(d.tail[4]) << 32 fallthrough case 4: k1 ^= uint64(d.tail[3]) << 24 fallthrough case 3: k1 ^= uint64(d.tail[2]) << 16 fallthrough case 2: k1 ^= uint64(d.tail[1]) << 8 fallthrough case 1: k1 ^= uint64(d.tail[0]) << 0 k1 *= c1_128 k1 = (k1 << 31) | (k1 >> 33) // rotl64(k1, 31) k1 *= c2_128 h1 ^= k1 } h1 ^= uint64(d.clen) h2 ^= uint64(d.clen) h1 += h2 h2 += h1 h1 = fmix64(h1) h2 = fmix64(h2) h1 += h2 h2 += h1 return h1, h2 } func fmix64(k uint64) uint64 { k ^= k >> 33 k *= 0xff51afd7ed558ccd k ^= k >> 33 k *= 0xc4ceb9fe1a85ec53 k ^= k >> 33 return k } /* func rotl64(x uint64, r byte) uint64 { return (x << r) | (x >> (64 - r)) } */ // Sum128 returns the MurmurHash3 sum of data. It is equivalent to the // following sequence (without the extra burden and the extra allocation): // hasher := New128() // hasher.Write(data) // return hasher.Sum128() func Sum128(data []byte) (h1 uint64, h2 uint64) { d := &digest128{h1: 0, h2: 0} d.tail = d.bmix(data) d.clen = len(data) return d.Sum128() } ubuntu-push-0.68+16.04.20160310.2/external/murmur3/murmur64.go0000644000015600001650000000170612670364255023745 0ustar pbuserpbgroup00000000000000package murmur3 import ( "hash" ) // Make sure interfaces are correctly implemented. var ( _ hash.Hash = new(digest64) _ hash.Hash64 = new(digest64) _ bmixer = new(digest64) ) // digest64 is half a digest128. type digest64 digest128 func New64() hash.Hash64 { d := (*digest64)(New128().(*digest128)) return d } func (d *digest64) Sum(b []byte) []byte { h1 := d.h1 return append(b, byte(h1>>56), byte(h1>>48), byte(h1>>40), byte(h1>>32), byte(h1>>24), byte(h1>>16), byte(h1>>8), byte(h1)) } func (d *digest64) Sum64() uint64 { h1, _ := (*digest128)(d).Sum128() return h1 } // Sum64 returns the MurmurHash3 sum of data. It is equivalent to the // following sequence (without the extra burden and the extra allocation): // hasher := New64() // hasher.Write(data) // return hasher.Sum64() func Sum64(data []byte) uint64 { d := &digest128{h1: 0, h2: 0} d.tail = d.bmix(data) d.clen = len(data) h1, _ := d.Sum128() return h1 } ubuntu-push-0.68+16.04.20160310.2/external/murmur3/LICENSE0000644000015600001650000000273012670364255022720 0ustar pbuserpbgroup00000000000000Copyright 2013, Sébastien Paolacci. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the library nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ubuntu-push-0.68+16.04.20160310.2/external/murmur3/murmur.go0000644000015600001650000000303012670364255023563 0ustar pbuserpbgroup00000000000000// Copyright 2013, Sébastien Paolacci. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* Native (and fast) implementation of Austin Appleby's MurmurHash3. Package murmur3 implements Austin Appleby's non-cryptographic MurmurHash3. Reference implementation: http://code.google.com/p/smhasher/wiki/MurmurHash3 History, characteristics and (legacy) perfs: https://sites.google.com/site/murmurhash/ https://sites.google.com/site/murmurhash/statistics */ package murmur3 type bmixer interface { bmix(p []byte) (tail []byte) Size() (n int) reset() } type digest struct { clen int // Digested input cumulative length. tail []byte // 0 to Size()-1 bytes view of `buf'. buf [16]byte // Expected (but not required) to be Size() large. bmixer } func (d *digest) BlockSize() int { return 1 } func (d *digest) Write(p []byte) (n int, err error) { n = len(p) d.clen += n if len(d.tail) > 0 { // Stick back pending bytes. nfree := d.Size() - len(d.tail) // nfree ∈ [1, d.Size()-1]. if nfree < len(p) { // One full block can be formed. block := append(d.tail, p[:nfree]...) p = p[nfree:] _ = d.bmix(block) // No tail. } else { // Tail's buf is large enough to prevent reallocs. p = append(d.tail, p...) } } d.tail = d.bmix(p) // Keep own copy of the 0 to Size()-1 pending bytes. nn := copy(d.buf[:], d.tail) d.tail = d.buf[:nn] return n, nil } func (d *digest) Reset() { d.clen = 0 d.tail = nil d.bmixer.reset() } ubuntu-push-0.68+16.04.20160310.2/external/murmur3/murmur_test.go0000644000015600001650000001125412670364255024631 0ustar pbuserpbgroup00000000000000package murmur3 import ( "hash" "testing" ) var data = []struct { h32 uint32 h64_1 uint64 h64_2 uint64 s string }{ {0x00000000, 0x0000000000000000, 0x0000000000000000, ""}, {0x248bfa47, 0xcbd8a7b341bd9b02, 0x5b1e906a48ae1d19, "hello"}, {0x149bbb7f, 0x342fac623a5ebc8e, 0x4cdcbc079642414d, "hello, world"}, {0xe31e8a70, 0xb89e5988b737affc, 0x664fc2950231b2cb, "19 Jan 2038 at 3:14:07 AM"}, {0xd5c48bfc, 0xcd99481f9ee902c9, 0x695da1a38987b6e7, "The quick brown fox jumps over the lazy dog."}, } func TestRef(t *testing.T) { for _, elem := range data { var h32 hash.Hash32 = New32() h32.Write([]byte(elem.s)) if v := h32.Sum32(); v != elem.h32 { t.Errorf("'%s': 0x%x (want 0x%x)", elem.s, v, elem.h32) } if v := Sum32([]byte(elem.s)); v != elem.h32 { t.Errorf("'%s': 0x%x (want 0x%x)", elem.s, v, elem.h32) } var h64 hash.Hash64 = New64() h64.Write([]byte(elem.s)) if v := h64.Sum64(); v != elem.h64_1 { t.Errorf("'%s': 0x%x (want 0x%x)", elem.s, v, elem.h64_1) } if v := Sum64([]byte(elem.s)); v != elem.h64_1 { t.Errorf("'%s': 0x%x (want 0x%x)", elem.s, v, elem.h64_1) } var h128 Hash128 = New128() h128.Write([]byte(elem.s)) if v1, v2 := h128.Sum128(); v1 != elem.h64_1 || v2 != elem.h64_2 { t.Errorf("'%s': 0x%x-0x%x (want 0x%x-0x%x)", elem.s, v1, v2, elem.h64_1, elem.h64_2) } if v1, v2 := Sum128([]byte(elem.s)); v1 != elem.h64_1 || v2 != elem.h64_2 { t.Errorf("'%s': 0x%x-0x%x (want 0x%x-0x%x)", elem.s, v1, v2, elem.h64_1, elem.h64_2) } } } func TestIncremental(t *testing.T) { for _, elem := range data { h32 := New32() h128 := New128() for i, j, k := 0, 0, len(elem.s); i < k; i = j { j = 2*i + 3 if j > k { j = k } s := elem.s[i:j] print(s + "|") h32.Write([]byte(s)) h128.Write([]byte(s)) } println() if v := h32.Sum32(); v != elem.h32 { t.Errorf("'%s': 0x%x (want 0x%x)", elem.s, v, elem.h32) } if v1, v2 := h128.Sum128(); v1 != elem.h64_1 || v2 != elem.h64_2 { t.Errorf("'%s': 0x%x-0x%x (want 0x%x-0x%x)", elem.s, v1, v2, elem.h64_1, elem.h64_2) } } } //--- func bench32(b *testing.B, length int) { buf := make([]byte, length) b.SetBytes(int64(length)) b.ResetTimer() for i := 0; i < b.N; i++ { Sum32(buf) } } func Benchmark32_1(b *testing.B) { bench32(b, 1) } func Benchmark32_2(b *testing.B) { bench32(b, 2) } func Benchmark32_4(b *testing.B) { bench32(b, 4) } func Benchmark32_8(b *testing.B) { bench32(b, 8) } func Benchmark32_16(b *testing.B) { bench32(b, 16) } func Benchmark32_32(b *testing.B) { bench32(b, 32) } func Benchmark32_64(b *testing.B) { bench32(b, 64) } func Benchmark32_128(b *testing.B) { bench32(b, 128) } func Benchmark32_256(b *testing.B) { bench32(b, 256) } func Benchmark32_512(b *testing.B) { bench32(b, 512) } func Benchmark32_1024(b *testing.B) { bench32(b, 1024) } func Benchmark32_2048(b *testing.B) { bench32(b, 2048) } func Benchmark32_4096(b *testing.B) { bench32(b, 4096) } func Benchmark32_8192(b *testing.B) { bench32(b, 8192) } //--- func benchPartial32(b *testing.B, length int) { buf := make([]byte, length) b.SetBytes(int64(length)) start := (32 / 8) / 2 chunks := 7 k := length / chunks tail := (length - start) % k b.ResetTimer() for i := 0; i < b.N; i++ { hasher := New32() hasher.Write(buf[0:start]) for j := start; j+k <= length; j += k { hasher.Write(buf[j : j+k]) } hasher.Write(buf[length-tail:]) hasher.Sum32() } } func BenchmarkPartial32_8(b *testing.B) { benchPartial32(b, 8) } func BenchmarkPartial32_16(b *testing.B) { benchPartial32(b, 16) } func BenchmarkPartial32_32(b *testing.B) { benchPartial32(b, 32) } func BenchmarkPartial32_64(b *testing.B) { benchPartial32(b, 64) } func BenchmarkPartial32_128(b *testing.B) { benchPartial32(b, 128) } //--- func bench128(b *testing.B, length int) { buf := make([]byte, length) b.SetBytes(int64(length)) b.ResetTimer() for i := 0; i < b.N; i++ { Sum128(buf) } } func Benchmark128_1(b *testing.B) { bench128(b, 1) } func Benchmark128_2(b *testing.B) { bench128(b, 2) } func Benchmark128_4(b *testing.B) { bench128(b, 4) } func Benchmark128_8(b *testing.B) { bench128(b, 8) } func Benchmark128_16(b *testing.B) { bench128(b, 16) } func Benchmark128_32(b *testing.B) { bench128(b, 32) } func Benchmark128_64(b *testing.B) { bench128(b, 64) } func Benchmark128_128(b *testing.B) { bench128(b, 128) } func Benchmark128_256(b *testing.B) { bench128(b, 256) } func Benchmark128_512(b *testing.B) { bench128(b, 512) } func Benchmark128_1024(b *testing.B) { bench128(b, 1024) } func Benchmark128_2048(b *testing.B) { bench128(b, 2048) } func Benchmark128_4096(b *testing.B) { bench128(b, 4096) } func Benchmark128_8192(b *testing.B) { bench128(b, 8192) } //--- ubuntu-push-0.68+16.04.20160310.2/external/murmur3/murmur32.go0000644000015600001650000000535012670364255023737 0ustar pbuserpbgroup00000000000000package murmur3 // http://code.google.com/p/guava-libraries/source/browse/guava/src/com/google/common/hash/Murmur3_32HashFunction.java import ( "hash" "unsafe" ) // Make sure interfaces are correctly implemented. var ( _ hash.Hash = new(digest32) _ hash.Hash32 = new(digest32) ) const ( c1_32 uint32 = 0xcc9e2d51 c2_32 uint32 = 0x1b873593 ) // digest32 represents a partial evaluation of a 32 bites hash. type digest32 struct { digest h1 uint32 // Unfinalized running hash. } func New32() hash.Hash32 { d := new(digest32) d.bmixer = d d.Reset() return d } func (d *digest32) Size() int { return 4 } func (d *digest32) reset() { d.h1 = 0 } func (d *digest32) Sum(b []byte) []byte { h := d.h1 return append(b, byte(h>>24), byte(h>>16), byte(h>>8), byte(h)) } // Digest as many blocks as possible. func (d *digest32) bmix(p []byte) (tail []byte) { h1 := d.h1 nblocks := len(p) / 4 for i := 0; i < nblocks; i++ { k1 := *(*uint32)(unsafe.Pointer(&p[i*4])) k1 *= c1_32 k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15) k1 *= c2_32 h1 ^= k1 h1 = (h1 << 13) | (h1 >> 19) // rotl32(h1, 13) h1 = h1*5 + 0xe6546b64 } d.h1 = h1 return p[nblocks*d.Size():] } func (d *digest32) Sum32() (h1 uint32) { h1 = d.h1 var k1 uint32 switch len(d.tail) & 3 { case 3: k1 ^= uint32(d.tail[2]) << 16 fallthrough case 2: k1 ^= uint32(d.tail[1]) << 8 fallthrough case 1: k1 ^= uint32(d.tail[0]) k1 *= c1_32 k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15) k1 *= c2_32 h1 ^= k1 } h1 ^= uint32(d.clen) h1 ^= h1 >> 16 h1 *= 0x85ebca6b h1 ^= h1 >> 13 h1 *= 0xc2b2ae35 h1 ^= h1 >> 16 return h1 } /* func rotl32(x uint32, r byte) uint32 { return (x << r) | (x >> (32 - r)) } */ // Sum32 returns the MurmurHash3 sum of data. It is equivalent to the // following sequence (without the extra burden and the extra allocation): // hasher := New32() // hasher.Write(data) // return hasher.Sum32() func Sum32(data []byte) uint32 { var h1 uint32 = 0 nblocks := len(data) / 4 var p uintptr if len(data) > 0 { p = uintptr(unsafe.Pointer(&data[0])) } p1 := p + uintptr(4*nblocks) for ; p < p1; p += 4 { k1 := *(*uint32)(unsafe.Pointer(p)) k1 *= c1_32 k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15) k1 *= c2_32 h1 ^= k1 h1 = (h1 << 13) | (h1 >> 19) // rotl32(h1, 13) h1 = h1*5 + 0xe6546b64 } tail := data[nblocks*4:] var k1 uint32 switch len(tail) & 3 { case 3: k1 ^= uint32(tail[2]) << 16 fallthrough case 2: k1 ^= uint32(tail[1]) << 8 fallthrough case 1: k1 ^= uint32(tail[0]) k1 *= c1_32 k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15) k1 *= c2_32 h1 ^= k1 } h1 ^= uint32(len(data)) h1 ^= h1 >> 16 h1 *= 0x85ebca6b h1 ^= h1 >> 13 h1 *= 0xc2b2ae35 h1 ^= h1 >> 16 return h1 } ubuntu-push-0.68+16.04.20160310.2/external/murmur3/README.md0000644000015600001650000000435212670364255023174 0ustar pbuserpbgroup00000000000000murmur3 ======= Native Go implementation of Austin Appleby's third MurmurHash revision (aka MurmurHash3). Reference algorithm has been slightly hacked as to support the streaming mode required by Go's standard [Hash interface](http://golang.org/pkg/hash/#Hash). Benchmarks ---------- Go tip as of 2013-03-11 (i.e almost go1.1), core i7 @ 3.4 Ghz. All runs include hasher instanciation and sequence finalization.

Benchmark32_1       200000000         8.4 ns/op       119.39 MB/s
Benchmark32_2       200000000         9.5 ns/op       211.69 MB/s
Benchmark32_4       500000000         7.9 ns/op       506.24 MB/s
Benchmark32_8       200000000         9.4 ns/op       853.40 MB/s
Benchmark32_16      100000000        12.1 ns/op      1324.19 MB/s
Benchmark32_32      100000000        18.2 ns/op      1760.81 MB/s
Benchmark32_64       50000000        31.2 ns/op      2051.59 MB/s
Benchmark32_128      50000000        58.7 ns/op      2180.34 MB/s
Benchmark32_256      20000000       116.0 ns/op      2194.85 MB/s
Benchmark32_512      10000000       227.0 ns/op      2247.43 MB/s
Benchmark32_1024      5000000       449.0 ns/op      2276.88 MB/s
Benchmark32_2048      2000000       894.0 ns/op      2289.87 MB/s
Benchmark32_4096      1000000      1792.0 ns/op      2284.64 MB/s
Benchmark32_8192       500000      3559.0 ns/op      2301.33 MB/s

Benchmark128_1       50000000        33.2 ns/op        30.15 MB/s
Benchmark128_2       50000000        33.3 ns/op        59.99 MB/s
Benchmark128_4       50000000        35.4 ns/op       112.88 MB/s
Benchmark128_8       50000000        36.6 ns/op       218.30 MB/s
Benchmark128_16      50000000        35.5 ns/op       450.86 MB/s
Benchmark128_32      50000000        35.3 ns/op       905.84 MB/s
Benchmark128_64      50000000        44.3 ns/op      1443.76 MB/s
Benchmark128_128     50000000        58.2 ns/op      2201.02 MB/s
Benchmark128_256     20000000        85.3 ns/op      2999.88 MB/s
Benchmark128_512     10000000       142.0 ns/op      3592.97 MB/s
Benchmark128_1024    10000000       258.0 ns/op      3963.74 MB/s
Benchmark128_2048     5000000       494.0 ns/op      4144.65 MB/s
Benchmark128_4096     2000000       955.0 ns/op      4285.80 MB/s
Benchmark128_8192     1000000      1884.0 ns/op      4347.12 MB/s

ubuntu-push-0.68+16.04.20160310.2/external/README0000644000015600001650000000020412670364255021153 0ustar pbuserpbgroup00000000000000Directly included vendorized small packages. * murmur3 comes from import at lp:~ubuntu-push-hackers/ubuntu-push/murmur at revno 12 ubuntu-push-0.68+16.04.20160310.2/tarmac_tests.sh0000755000015600001650000000017012670364255021503 0ustar pbuserpbgroup00000000000000#!/bin/bash # For running tests in Jenkins by Tarmac set -e make bootstrap make check make check-race make check-format ubuntu-push-0.68+16.04.20160310.2/signing-helper/0000755000015600001650000000000012670364532021366 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/signing-helper/signing-helper.cpp0000644000015600001650000000563712670364255025022 0ustar pbuserpbgroup00000000000000/* * Copyright (C) 2013-2015 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 . * * In addition, as a special exception, the copyright holders give * permission to link the code of portions of this program with the * OpenSSL library under certain conditions as described in each * individual source file, and distribute linked combinations * including the two. * You must obey the GNU General Public License in all respects * for all of the code used other than OpenSSL. If you modify * file(s) with this exception, you may extend this exception to your * version of the file(s), but you are not obligated to do so. If you * do not wish to do so, delete this exception statement from your * version. If you delete this exception statement from all source * files in the program, then also delete it here. */ #include "signing.h" #include #include #include #include #include #include #include namespace UbuntuOne { SigningExample::SigningExample(QObject *parent, const QString& url) : QObject(parent), m_url(url), m_method("POST") { QObject::connect(&service, SIGNAL(credentialsFound(const Token&)), this, SLOT(handleCredentialsFound(Token))); QObject::connect(&service, SIGNAL(credentialsNotFound()), this, SLOT(handleCredentialsNotFound())); } SigningExample::~SigningExample(){ } void SigningExample::setMethod(const QString& method) { m_method = method; } void SigningExample::doExample() { service.getCredentials(); } void SigningExample::handleCredentialsFound(Token token) { qDebug() << "Credentials found, signing url."; std::cout << token.signUrl(m_url, m_method).toStdString(); QCoreApplication::instance()->exit(0); } void SigningExample::handleCredentialsNotFound() { qDebug() << "No credentials were found."; QCoreApplication::instance()->exit(1); } } // namespace UbuntuOne int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); if (argc < 2) { return 2; } UbuntuOne::SigningExample *example = new UbuntuOne::SigningExample(&a, argv[1]); if (argc == 3) { example->setMethod(argv[2]); } QObject::connect(example, SIGNAL(finished()), &a, SLOT(quit())); QTimer::singleShot(0, example, SLOT(doExample())); return a.exec(); } ubuntu-push-0.68+16.04.20160310.2/signing-helper/CMakeLists.txt0000644000015600001650000000206112670364255024127 0ustar pbuserpbgroup00000000000000cmake_minimum_required(VERSION 2.8) SET (EXAMPLES_TARGET ubuntuoneauth-examples) SET (SIGNING_EXE "signing-helper") find_package (PkgConfig REQUIRED) pkg_check_modules(UBUNTUONE REQUIRED ubuntuoneauth-2.0) add_definitions(${UBUNTUONE_CFLAGS} ${UBUNTUONE_CFLAGS_OTHER}) # Qt5 bits SET (CMAKE_INCLUDE_CURRENT_DIR ON) SET (CMAKE_AUTOMOC ON) find_package(Qt5Core REQUIRED) SET (SIGNING_SOURCES signing-helper.cpp) SET (SIGNING_HEADERS signing.h) add_executable (${SIGNING_EXE} ${SIGNING_SOURCES} ${SIGNING_HEADERS}) qt5_use_modules (${SIGNING_EXE} DBus Network) target_link_libraries (${SIGNING_EXE} ${UBUNTUONE_LDFLAGS}) add_custom_target(examples-valgrind COMMAND "valgrind --tool=memcheck ${CMAKE_CURRENT_BINARY_DIR}/${SIGNING_EXE}" DEPENDS ${SIGNING_EXE} ) add_custom_target(examples-valgrind-leaks COMMAND "valgrind --tool=memcheck --track-origins=yes --num-callers=40 --leak-resolution=high --leak-check=full ${CMAKE_CURRENT_BINARY_DIR}/${SIGNING_EXE}" DEPENDS ${SIGNING_EXE} ) INSTALL_TARGETS( "lib/ubuntu-push-client/" ${SIGNING_EXE}) ubuntu-push-0.68+16.04.20160310.2/signing-helper/signing.h0000644000015600001650000000421312670364255023177 0ustar pbuserpbgroup00000000000000/* * Copyright (C) 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 . * * In addition, as a special exception, the copyright holders give * permission to link the code of portions of this program with the * OpenSSL library under certain conditions as described in each * individual source file, and distribute linked combinations * including the two. * You must obey the GNU General Public License in all respects * for all of the code used other than OpenSSL. If you modify * file(s) with this exception, you may extend this exception to your * version of the file(s), but you are not obligated to do so. If you * do not wish to do so, delete this exception statement from your * version. If you delete this exception statement from all source * files in the program, then also delete it here. */ #ifndef _SIGNING_H_ #define _SIGNING_H_ #include #include #include #include #include "ssoservice.h" #include "token.h" #include "requests.h" #include "errormessages.h" namespace UbuntuOne { class SigningExample : public QObject { Q_OBJECT public: explicit SigningExample(QObject *parent = 0, const QString& url="https://one.ubuntu.com/api/account/"); ~SigningExample(); void setMethod(const QString& method); public slots: void doExample(); signals: void finished(); private slots: void handleCredentialsFound(Token token); void handleCredentialsNotFound(); private: SSOService service; QNetworkAccessManager nam; QString m_url; QString m_method; }; } #endif /* _SIGNING_H_ */ ubuntu-push-0.68+16.04.20160310.2/.precommit0000755000015600001650000000210712670364255020460 0ustar pbuserpbgroup00000000000000#!/bin/sh set -e echo "$@" # put me in the project root, call me ".precommit". # And put this here-document in ~/.bazaar/plugins/precommit_script.py: < #include #include // a small wrapper because cgo doesn't handle varargs char *cuote (const char *id) { return nih_dbus_path (NULL, "", id, NULL); } */ import "C" import ( "unsafe" ) func Quote(s []byte) string { cs := C.CString(string(s)) defer C.free(unsafe.Pointer(cs)) cq := C.cuote(cs) defer C.nih_free(unsafe.Pointer(cq)) return C.GoString(cq)[1:] } ubuntu-push-0.68+16.04.20160310.2/nih/nih.go0000644000015600001650000000327512670364255020347 0ustar pbuserpbgroup00000000000000/* 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 nih reimplements libnih-dbus's nih_dbus_path's path element // quoting. // // Reimplementing libnih is a wonderful exercise that everybody should persue // at least thrice. package nih import "strconv" // Quote() takes a byte slice and quotes it á la libnih. func Quote(s []byte) []byte { if len(s) == 0 { return []byte{'_'} } out := make([]byte, 0, 2*len(s)) for _, c := range s { if ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') { out = append(out, c) } else { if c < 16 { out = append(out, '_', '0') } else { out = append(out, '_') } out = strconv.AppendUint(out, uint64(c), 16) } } return out } // Unquote() takes a byte slice and undoes the damage done to it by Quote(). func Unquote(s []byte) []byte { out := make([]byte, 0, len(s)) for i := 0; i < len(s); i++ { if s[i] == '_' { if len(s) < i+3 { break } num, err := strconv.ParseUint(string(s[i+1:i+3]), 16, 8) if err == nil { out = append(out, byte(num)) } i += 2 } else { out = append(out, s[i]) } } return out } ubuntu-push-0.68+16.04.20160310.2/nih/nih_test.go0000644000015600001650000000350412670364255021401 0ustar pbuserpbgroup00000000000000/* 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 nih import ( "testing" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/nih/cnih" ) func TestNIH(t *testing.T) { TestingT(t) } type nihSuite struct{} var _ = Suite(&nihSuite{}) func (ns *nihSuite) TestQuote(c *C) { for i, s := range []struct { raw []byte quoted []byte }{ {[]byte("test"), []byte("test")}, {[]byte("foo/bar.baz"), []byte("foo_2fbar_2ebaz")}, {[]byte("test_thing"), []byte("test_5fthing")}, {[]byte("\x01\x0f\x10\xff"), []byte("_01_0f_10_ff")}, {[]byte{}, []byte{'_'}}, } { c.Check(string(s.quoted), Equals, cnih.Quote(s.raw), Commentf("iter %d (%s)", i, string(s.quoted))) c.Check(string(Quote(s.raw)), DeepEquals, string(s.quoted), Commentf("iter %d (%s)", i, string(s.quoted))) c.Check(Unquote(s.quoted), DeepEquals, s.raw, Commentf("iter %d (%s)", i, string(s.quoted))) c.Check(string(Quote(s.raw)), Equals, cnih.Quote(s.raw), Commentf("iter %d (%s)", i, string(s.quoted))) } // check one cnih doesn't like c.Check(Quote([]byte{0}), DeepEquals, []byte("_00")) // check we don't panic with some weird ones for i, s := range []string{"foo_", "foo_a", "foo_zz"} { c.Check(Unquote([]byte(s)), DeepEquals, []byte("foo"), Commentf("iter %d (%s)", i, s)) } } ubuntu-push-0.68+16.04.20160310.2/PACKAGE_DEPS0000644000015600001650000000046412670364255020252 0ustar pbuserpbgroup00000000000000# See the README for what this file is and how to use it. build-essential cmake libdbus-1-dev libgcrypt11-dev libglib2.0-dev libnih-dbus-dev libsqlite3-dev libubuntuoneauth-2.0-dev libmessaging-menu-dev libubuntu-app-launch2-dev libclick-0.4-dev liburl-dispatcher1-dev libaccounts-glib-dev system-image-dbus ubuntu-push-0.68+16.04.20160310.2/ubuntu-push-client.go0000644000015600001650000000301012670364255022551 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package main import ( "log" "os" "os/signal" "runtime" "syscall" "launchpad.net/go-xdg/v0" "launchpad.net/ubuntu-push/client" ) func installSigQuitHandler() { go func() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGQUIT) buf := make([]byte, 1<<20) for { <-sigs sz := runtime.Stack(buf, true) log.Printf("=== received SIGQUIT ===\n*** goroutine dump...\n%s\n*** end", buf[:sz]) } }() } func main() { installSigQuitHandler() cfgFname, err := xdg.Config.Find("ubuntu-push-client/config.json") if err != nil { log.Fatalf("unable to find a configuration file: %v", err) } lvlFname, err := xdg.Data.Ensure("ubuntu-push-client/levels.db") if err != nil { log.Fatalf("unable to open the levels database: %v", err) } cli := client.NewPushClient(cfgFname, lvlFname) err = cli.Start() if err != nil { log.Fatalf("unable to start: %v", err) } cli.Loop() } ubuntu-push-0.68+16.04.20160310.2/logger/0000755000015600001650000000000012670364532017732 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/logger/logger.go0000644000015600001650000001031112670364255021536 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ // Package logger defines a simple logger API with level of logging control. package logger import ( "fmt" "io" "log" "os" "runtime" "launchpad.net/ubuntu-push/config" ) // Logger is a simple logger interface with logging at levels. type Logger interface { // Re-expose base Output for logging events. Output(calldept int, s string) error // Errorf logs an error. Errorf(format string, v ...interface{}) // Fatalf logs an error and exits the program with os.Exit(1). Fatalf(format string, v ...interface{}) // PanicStackf logs an error message and a stacktrace, for use // in panic recovery. PanicStackf(format string, v ...interface{}) // Infof logs an info message. Infof(format string, v ...interface{}) // Debugf logs a debug message. Debugf(format string, v ...interface{}) } type simpleLogger struct { outputFunc func(calldepth int, s string) error nlevel int } const ( calldepthBase = 3 lError = iota lInfo lDebug ) var levelToNLevel = map[string]int{ "error": lError, "info": lInfo, "debug": lDebug, } // MinimalLogger is the minimal interface required to build a simple logger. type MinimalLogger interface { Output(calldepth int, s string) error } // NewSimpleLoggerFromMinimalLogger creates a logger logging only up // to the given level. The level can be, in order: "error", "info", // "debug". It takes a value just implementing stlib Logger.Output(). func NewSimpleLoggerFromMinimalLogger(minLog MinimalLogger, level string) Logger { nlevel := levelToNLevel[level] return &simpleLogger{ minLog.Output, nlevel, } } // NewSimpleLogger creates a logger logging only up to the given // level. The level can be, in order: "error", "info", "debug". It takes an // io.Writer. func NewSimpleLogger(w io.Writer, level string) Logger { flags := log.Ldate | log.Ltime | log.Lmicroseconds if levelToNLevel[level] >= lDebug { flags = flags | log.Lshortfile } return NewSimpleLoggerFromMinimalLogger( log.New(w, "", flags), level, ) } func (lg *simpleLogger) Output(calldepth int, s string) error { return lg.outputFunc(calldepth+2, s) } func (lg *simpleLogger) Errorf(format string, v ...interface{}) { lg.outputFunc(calldepthBase, fmt.Sprintf("ERROR "+format, v...)) } var osExit = os.Exit // for testing func (lg *simpleLogger) Fatalf(format string, v ...interface{}) { lg.outputFunc(calldepthBase, fmt.Sprintf("ERROR "+format, v...)) osExit(1) } func (lg *simpleLogger) PanicStackf(format string, v ...interface{}) { msg := fmt.Sprintf(format, v...) stack := make([]byte, 8*1024) // Stack writes less but doesn't fail stackWritten := runtime.Stack(stack, false) stack = stack[:stackWritten] lg.outputFunc(calldepthBase, fmt.Sprintf("ERROR(PANIC) %s:\n%s", msg, stack)) } func (lg *simpleLogger) Infof(format string, v ...interface{}) { if lg.nlevel >= lInfo { lg.outputFunc(calldepthBase, fmt.Sprintf("INFO "+format, v...)) } } func (lg *simpleLogger) Debugf(format string, v ...interface{}) { if lg.nlevel >= lDebug { lg.outputFunc(calldepthBase, fmt.Sprintf("DEBUG "+format, v...)) } } // config bits // ConfigLogLevel can hold a log level in a configuration struct. type ConfigLogLevel string func (cll *ConfigLogLevel) ConfigFromJSONString() {} func (cll *ConfigLogLevel) UnmarshalJSON(b []byte) error { return config.UnmarshalJSONViaString(cll, b) } func (cll *ConfigLogLevel) SetFromString(enc string) error { _, ok := levelToNLevel[enc] if !ok { return fmt.Errorf("not a log level: %s", enc) } *cll = ConfigLogLevel(enc) return nil } // Level returns the log level string held in cll. func (cll ConfigLogLevel) Level() string { return string(cll) } ubuntu-push-0.68+16.04.20160310.2/logger/logger_test.go0000644000015600001650000001146712670364255022612 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package logger import ( "bytes" "fmt" "log" "os" "runtime" "testing" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/config" ) func TestLogger(t *testing.T) { TestingT(t) } type loggerSuite struct{} var _ = Suite(&loggerSuite{}) func (s *loggerSuite) TestErrorf(c *C) { buf := &bytes.Buffer{} logger := NewSimpleLogger(buf, "error") logger.Errorf("%v %d", "error", 1) c.Check(buf.String(), Matches, ".* ERROR error 1\n") } func (s *loggerSuite) TestFatalf(c *C) { defer func() { osExit = os.Exit }() var exitCode int osExit = func(code int) { exitCode = code } buf := &bytes.Buffer{} logger := NewSimpleLogger(buf, "error") logger.Fatalf("%v %v", "error", "fatal") c.Check(buf.String(), Matches, ".* ERROR error fatal\n") c.Check(exitCode, Equals, 1) } func (s *loggerSuite) TestInfof(c *C) { buf := &bytes.Buffer{} logger := NewSimpleLogger(buf, "info") logger.Infof("%v %d", "info", 1) c.Check(buf.String(), Matches, ".* INFO info 1\n") } func (s *loggerSuite) TestDebugf(c *C) { buf := &bytes.Buffer{} logger := NewSimpleLogger(buf, "debug") logger.Debugf("%v %d", "debug", 1) c.Check(buf.String(), Matches, `.* DEBUG debug 1\n`) } func (s *loggerSuite) TestFormat(c *C) { buf := &bytes.Buffer{} logger := NewSimpleLogger(buf, "error") logger.Errorf("%v %d", "error", 2) c.Check(buf.String(), Matches, `.* .*\.\d+ ERROR error 2\n`) } func (s *loggerSuite) TestLevel(c *C) { buf := &bytes.Buffer{} logger := NewSimpleLogger(buf, "error") logger.Errorf("%s%d", "e", 3) logger.Infof("%s%d", "i", 3) logger.Debugf("%s%d", "d", 3) c.Check(buf.String(), Matches, `.* ERROR e3\n`) buf.Reset() logger = NewSimpleLogger(buf, "info") logger.Errorf("%s%d", "e", 4) logger.Debugf("%s%d", "d", 4) logger.Infof("%s%d", "i", 4) c.Check(buf.String(), Matches, `.* ERROR e4\n.* INFO i4\n`) buf.Reset() logger = NewSimpleLogger(buf, "debug") logger.Errorf("%s%d", "e", 5) logger.Debugf("%s%d", "d", 5) logger.Infof("%s%d", "i", 5) c.Check(buf.String(), Matches, `.* ERROR e5\n.* DEBUG d5\n.* INFO i5\n`) } func panicAndRecover(logger Logger, n int, doPanic bool, line *int, ok *bool) { defer func() { if err := recover(); err != nil { logger.PanicStackf("%v %d", err, n) } }() _, _, *line, *ok = runtime.Caller(0) if doPanic { panic("Troubles") // @ line + 2 } } func (s *loggerSuite) TestPanicStackfPanicScenario(c *C) { buf := &bytes.Buffer{} logger := NewSimpleLogger(buf, "error") var line int var ok bool panicAndRecover(logger, 6, true, &line, &ok) c.Assert(ok, Equals, true) c.Check(buf.String(), Matches, fmt.Sprintf("(?s).* ERROR\\(PANIC\\) Troubles 6:.*panicAndRecover.*logger_test.go:%d.*", line+2)) } func (s *loggerSuite) TestPanicStackfNoPanicScenario(c *C) { buf := &bytes.Buffer{} logger := NewSimpleLogger(buf, "error") var line int var ok bool panicAndRecover(logger, 6, false, &line, &ok) c.Check(buf.String(), Equals, "") } func (s *loggerSuite) TestReexposeOutput(c *C) { buf := &bytes.Buffer{} baselog := log.New(buf, "", log.Lshortfile) logger := NewSimpleLoggerFromMinimalLogger(baselog, "error") baselog.Output(1, "foobar") logger.Output(1, "foobaz") c.Check(buf.String(), Matches, "logger_test.go:[0-9]+: foobar\nlogger_test.go:[0-9]+: foobaz\n") } type testLogLevelConfig struct { Lvl ConfigLogLevel } func (s *loggerSuite) TestReadConfigLogLevel(c *C) { buf := bytes.NewBufferString(`{"lvl": "debug"}`) var cfg testLogLevelConfig err := config.ReadConfig(buf, &cfg) c.Assert(err, IsNil) c.Check(cfg.Lvl.Level(), Equals, "debug") } func (s *loggerSuite) TestReadConfigLogLevelErrors(c *C) { var cfg testLogLevelConfig checkError := func(jsonCfg string, expectedError string) { buf := bytes.NewBufferString(jsonCfg) err := config.ReadConfig(buf, &cfg) c.Check(err, ErrorMatches, expectedError) } checkError(`{"lvl": 1}`, "lvl:.*type string") checkError(`{"lvl": "foo"}`, "lvl: not a log level: foo") } func (s *loggerSuite) TestLogLineNo(c *C) { buf := &bytes.Buffer{} logger := NewSimpleLogger(buf, "debug") logger.Output(1, "foobaz") c.Check(buf.String(), Matches, ".* .* logger_test.go:[0-9]+: foobaz\n") buf.Reset() logger = NewSimpleLogger(buf, "error") logger.Output(1, "foobaz") c.Check(buf.String(), Matches, ".* .* foobaz\n") } ubuntu-push-0.68+16.04.20160310.2/accounts/0000755000015600001650000000000012670364532020272 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/accounts/caccounts.go0000644000015600001650000000132012670364255022601 0ustar pbuserpbgroup00000000000000package accounts /* #include void gocb(); static void cb(AgManager *manager, AgAccountId account_id, gpointer p) { AgAccount *account = ag_manager_get_account(manager, account_id); if (!account) { return; } GList *services = ag_account_list_services(account); if (!services || !services->data) { return; } gocb(); } void start() { AgManager *manager = ag_manager_new_for_service_type("ubuntuone"); g_signal_connect(manager, "account-created", G_CALLBACK(cb), NULL); g_signal_connect(manager, "account-deleted", G_CALLBACK(cb), NULL); g_signal_connect(manager, "account-updated", G_CALLBACK(cb), NULL); } */ import "C" ubuntu-push-0.68+16.04.20160310.2/accounts/accounts.go0000644000015600001650000000163312670364255022445 0ustar pbuserpbgroup00000000000000/* 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 accounts wraps libaccounts package accounts /* #cgo pkg-config: libaccounts-glib void start(); */ import "C" type Changed struct{} var ch chan Changed //export gocb func gocb() { ch <- Changed{} } func Watch() <-chan Changed { ch = make(chan Changed, 1) C.start() return ch } ubuntu-push-0.68+16.04.20160310.2/client/0000755000015600001650000000000012670364532017731 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/client/service/0000755000015600001650000000000012670364532021371 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/client/service/mbox_test.go0000644000015600001650000000735512670364255023740 0ustar pbuserpbgroup00000000000000/* 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 service import ( "encoding/json" "fmt" "strings" . "launchpad.net/gocheck" ) type mBoxSuite struct { prevMBoxMaxMessagesSize int } var _ = Suite(&mBoxSuite{}) func (s *mBoxSuite) SetUpSuite(c *C) { s.prevMBoxMaxMessagesSize = mBoxMaxMessagesSize mBoxMaxMessagesSize = 100 } func (s *mBoxSuite) TearDownSuite(c *C) { mBoxMaxMessagesSize = s.prevMBoxMaxMessagesSize } func (s *mBoxSuite) TestAppend(c *C) { mbox := &mBox{} m1 := json.RawMessage(`{"m":1}`) m2 := json.RawMessage(`{"m":2}`) mbox.Append(m1, "n1") mbox.Append(m2, "n2") c.Check(mbox.messages, DeepEquals, []string{string(m1), string(m2)}) c.Check(mbox.nids, DeepEquals, []string{"n1", "n2"}) } func (s *mBoxSuite) TestAllMessagesEmpty(c *C) { mbox := &mBox{} c.Check(mbox.AllMessages(), HasLen, 0) } func (s *mBoxSuite) TestAllMessages(c *C) { mbox := &mBox{} m1 := json.RawMessage(`{"m":1}`) m2 := json.RawMessage(`{"m":2}`) mbox.Append(m1, "n1") mbox.Append(m2, "n2") c.Check(mbox.AllMessages(), DeepEquals, []string{string(m1), string(m2)}) } func blobMessage(n int, sz int) json.RawMessage { return json.RawMessage(fmt.Sprintf(`{"n":%d,"b":"%s"}`, n, strings.Repeat("x", sz-14))) } func (s *mBoxSuite) TestAppendEvictSome(c *C) { mbox := &mBox{} m1 := blobMessage(1, 25) m2 := blobMessage(2, 25) m3 := blobMessage(3, 50) mbox.Append(m1, "n1") mbox.Append(m2, "n2") mbox.Append(m3, "n3") c.Check(mbox.curSize, Equals, 100) c.Check(mbox.evicted, Equals, 0) m4 := blobMessage(4, 23) mbox.Append(m4, "n4") c.Assert(mbox.evicted, Equals, 1) c.Check(mbox.curSize, Equals, 25+50+23) c.Check(mbox.AllMessages(), DeepEquals, []string{string(m2), string(m3), string(m4)}) c.Check(mbox.messages[:1], DeepEquals, []string{""}) c.Check(mbox.nids, DeepEquals, []string{"", "n2", "n3", "n4"}) } func (s *mBoxSuite) TestAppendEvictSomeCopyOver(c *C) { mbox := &mBox{} m1 := blobMessage(1, 25) m2 := blobMessage(2, 25) m3 := blobMessage(3, 25) m4 := blobMessage(4, 25) mbox.Append(m1, "n1") mbox.Append(m2, "n2") mbox.Append(m3, "n3") mbox.Append(m4, "n4") c.Check(mbox.curSize, Equals, 100) c.Check(mbox.evicted, Equals, 0) m5 := blobMessage(5, 40) mbox.Append(m5, "n5") c.Assert(mbox.evicted, Equals, 0) c.Check(mbox.curSize, Equals, 90) c.Check(mbox.AllMessages(), DeepEquals, []string{string(m3), string(m4), string(m5)}) c.Check(mbox.nids, DeepEquals, []string{"n3", "n4", "n5"}) // do it again m6 := blobMessage(6, 40) mbox.Append(m6, "n6") c.Assert(mbox.evicted, Equals, 0) c.Check(mbox.curSize, Equals, 80) c.Check(mbox.AllMessages(), DeepEquals, []string{string(m5), string(m6)}) c.Check(mbox.nids, DeepEquals, []string{"n5", "n6"}) } func (s *mBoxSuite) TestAppendEvictEverything(c *C) { mbox := &mBox{} m1 := blobMessage(1, 25) m2 := blobMessage(2, 25) m3 := blobMessage(3, 50) mbox.Append(m1, "n1") mbox.Append(m2, "n2") mbox.Append(m3, "n3") c.Check(mbox.curSize, Equals, 100) c.Check(mbox.evicted, Equals, 0) m4 := blobMessage(4, 90) mbox.Append(m4, "n4") c.Assert(mbox.evicted, Equals, 0) c.Check(mbox.curSize, Equals, 90) c.Check(mbox.AllMessages(), DeepEquals, []string{string(m4)}) c.Check(mbox.nids, DeepEquals, []string{"n4"}) } ubuntu-push-0.68+16.04.20160310.2/client/service/service.go0000644000015600001650000001262712670364255023372 0ustar pbuserpbgroup00000000000000/* 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 service import ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "net/url" "os" http13 "launchpad.net/ubuntu-push/http13client" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/nih" ) // PushServiceSetup encapsulates the params for setting up a PushService. type PushServiceSetup struct { RegURL *url.URL DeviceId string AuthGetter func(string) string InstalledChecker click.InstalledChecker } // PushService is the dbus api type PushService struct { DBusService regURL *url.URL deviceId string authGetter func(string) string httpCli http13.Client } var ( PushServiceBusAddress = bus.Address{ Interface: "com.ubuntu.PushNotifications", Path: "/com/ubuntu/PushNotifications", Name: "com.ubuntu.PushNotifications", } ) // NewPushService() builds a new service and returns it. func NewPushService(setup *PushServiceSetup, log logger.Logger) *PushService { var svc = &PushService{} svc.Log = log svc.Bus = bus.SessionBus.Endpoint(PushServiceBusAddress, log) svc.installedChecker = setup.InstalledChecker svc.regURL = setup.RegURL svc.deviceId = setup.DeviceId svc.authGetter = setup.AuthGetter return svc } // getAuthorization() returns the URL and the authorization header for // POSTing to the registration HTTP endpoint for op func (svc *PushService) getAuthorization(op string) (string, string) { if svc.authGetter == nil || svc.regURL == nil { return "", "" } purl, err := svc.regURL.Parse(op) if err != nil { panic("op to getAuthorization was invalid") } url := purl.String() return url, svc.authGetter(url) } func (svc *PushService) Start() error { return svc.DBusService.Start(bus.DispatchMap{ "Register": svc.register, "Unregister": svc.unregister, }, PushServiceBusAddress, nil) } var ( ErrBadServer = errors.New("bad server") ErrBadRequest = errors.New("bad request") ErrBadToken = errors.New("bad token") ErrBadAuth = errors.New("bad auth") ) type registrationRequest struct { DeviceId string `json:"deviceid"` AppId string `json:"appid"` } type registrationReply struct { Token string `json:"token"` // the bit we're after Ok bool `json:"ok"` // only ever true or absent Error string `json:"error"` // these two only used for debugging Message string `json:"message"` // } func (svc *PushService) manageReg(op, appId string) (*registrationReply, error) { req_body, err := json.Marshal(registrationRequest{svc.deviceId, appId}) if err != nil { return nil, fmt.Errorf("unable to marshal register request body: %v", err) } url, auth := svc.getAuthorization(op) if auth == "" { return nil, ErrBadAuth } req, err := http13.NewRequest("POST", url, bytes.NewReader(req_body)) if err != nil { panic(fmt.Errorf("unable to build register request: %v", err)) } req.Header.Add("Authorization", auth) req.Header.Add("Content-Type", "application/json") resp, err := svc.httpCli.Do(req) if err != nil { return nil, fmt.Errorf("unable to request registration: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { svc.Log.Errorf("register endpoint replied %d", resp.StatusCode) switch { case resp.StatusCode >= http.StatusInternalServerError: // XXX retry on 503 return nil, ErrBadServer case resp.StatusCode == http.StatusUnauthorized: return nil, ErrBadAuth default: return nil, ErrBadRequest } } // errors below here Can't Happen (tm). body, err := ioutil.ReadAll(resp.Body) if err != nil { svc.Log.Errorf("during ReadAll() of response body: %v", err) return nil, err } var reply registrationReply err = json.Unmarshal(body, &reply) if err != nil { svc.Log.Errorf("during Unmarshal of response body: %v", err) return nil, fmt.Errorf("unable to unmarshal register response: %v", err) } return &reply, nil } func (svc *PushService) register(path string, args, _ []interface{}) ([]interface{}, error) { app, err := svc.grabDBusPackageAndAppId(path, args, 0) if err != nil { return nil, err } rawAppId := string(nih.Quote([]byte(app.Original()))) rv := os.Getenv("PUSH_REG_" + rawAppId) if rv != "" { return []interface{}{rv}, nil } reply, err := svc.manageReg("/register", app.Original()) if err != nil { return nil, err } if !reply.Ok || reply.Token == "" { svc.Log.Errorf("unexpected response: %#v", reply) return nil, ErrBadToken } return []interface{}{reply.Token}, nil } func (svc *PushService) unregister(path string, args, _ []interface{}) ([]interface{}, error) { app, err := svc.grabDBusPackageAndAppId(path, args, 0) if err != nil { return nil, err } return nil, svc.Unregister(app.Original()) } func (svc *PushService) Unregister(appId string) error { _, err := svc.manageReg("/unregister", appId) return err } ubuntu-push-0.68+16.04.20160310.2/client/service/common.go0000644000015600001650000000677712670364255023233 0ustar pbuserpbgroup00000000000000/* 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 service implements the dbus-level service with which client // applications are expected to interact. package service import ( "errors" "strings" "sync" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/nih" ) type DBusService struct { lock sync.RWMutex state ServiceState installedChecker click.InstalledChecker Log logger.Logger Bus bus.Endpoint } // the service can be in a numnber of states type ServiceState uint8 const ( StateUnknown ServiceState = iota StateRunning // Start() has been successfully called StateFinished // Stop() has been successfully called ) var ( ErrNotConfigured = errors.New("not configured") ErrAlreadyStarted = errors.New("already started") ErrBadArgCount = errors.New("wrong number of arguments") ErrBadArgType = errors.New("bad argument type") ErrBadJSON = errors.New("bad json data") ErrAppIdMismatch = errors.New("package must be prefix of app id") ) // IsRunning() returns whether the service's state is StateRunning func (svc *DBusService) IsRunning() bool { svc.lock.RLock() defer svc.lock.RUnlock() return svc.state == StateRunning } // Start() dials the bus, grab the name, and listens for method calls. func (svc *DBusService) Start(dispatchMap bus.DispatchMap, busAddr bus.Address, init func() error) error { svc.lock.Lock() defer svc.lock.Unlock() if svc.state != StateUnknown { return ErrAlreadyStarted } if svc.Log == nil || svc.Bus == nil { return ErrNotConfigured } err := svc.Bus.Dial() if err != nil { return err } ch := svc.Bus.GrabName(true) log := svc.Log go func() { for err := range ch { if !svc.IsRunning() { break } if err != nil { log.Fatalf("name channel for %s got: %v", busAddr.Name, err) } } }() if init != nil { if err = init(); err != nil { return err } } svc.Bus.WatchMethod(dispatchMap, "/*", svc) svc.state = StateRunning return nil } // Stop() closes the bus and sets the state to StateFinished func (svc *DBusService) Stop() { svc.lock.Lock() defer svc.lock.Unlock() if svc.Bus != nil { svc.Bus.Close() } svc.state = StateFinished } // grabDBusPackageAndAppId() extracts the appId from a dbus-provided // []interface{}, and checks it against the package in the last // element of the dbus path. func (svc *DBusService) grabDBusPackageAndAppId(path string, args []interface{}, numExtra int) (app *click.AppId, err error) { if len(args) != 1+numExtra { return nil, ErrBadArgCount } id, ok := args[0].(string) if !ok { return nil, ErrBadArgType } pkgname := string(nih.Unquote([]byte(path[strings.LastIndex(path, "/")+1:]))) app, err = click.ParseAndVerifyAppId(id, svc.installedChecker) if err != nil { return nil, err } if !app.InPackage(pkgname) { return nil, ErrAppIdMismatch } return } ubuntu-push-0.68+16.04.20160310.2/client/service/service_test.go0000644000015600001650000003017312670364255024425 0ustar pbuserpbgroup00000000000000/* 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 service import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "testing" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/bus" testibus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/nih" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) func TestService(t *testing.T) { TestingT(t) } type serviceSuite struct { log logger.Logger bus bus.Endpoint } var _ = Suite(&serviceSuite{}) var ( aPackage = "com.example.test" anAppId = aPackage + "_test-number-one" aPackageOnBus = "/" + string(nih.Quote([]byte(aPackage))) ) func (ss *serviceSuite) SetUpTest(c *C) { ss.log = helpers.NewTestLogger(c, "debug") ss.bus = testibus.NewTestingEndpoint(condition.Work(true), nil) } var testSetup = &PushServiceSetup{} func (ss *serviceSuite) TestBuild(c *C) { setup := &PushServiceSetup{ RegURL: helpers.ParseURL("http://reg"), DeviceId: "FOO", AuthGetter: func(s string) string { return "" }, } svc := NewPushService(setup, ss.log) c.Check(svc.regURL, DeepEquals, helpers.ParseURL("http://reg")) c.Check(fmt.Sprintf("%#v", svc.authGetter), Equals, fmt.Sprintf("%#v", setup.AuthGetter)) // ... } func (ss *serviceSuite) TestStart(c *C) { svc := NewPushService(testSetup, ss.log) svc.Bus = ss.bus c.Check(svc.IsRunning(), Equals, false) c.Check(svc.Start(), IsNil) c.Check(svc.IsRunning(), Equals, true) svc.Stop() } func (ss *serviceSuite) TestStartTwice(c *C) { svc := NewPushService(testSetup, ss.log) svc.Bus = ss.bus c.Check(svc.Start(), IsNil) c.Check(svc.Start(), Equals, ErrAlreadyStarted) svc.Stop() } func (ss *serviceSuite) TestStartNoLog(c *C) { svc := NewPushService(testSetup, nil) svc.Bus = ss.bus c.Check(svc.Start(), Equals, ErrNotConfigured) } func (ss *serviceSuite) TestStartNoBus(c *C) { svc := NewPushService(testSetup, ss.log) svc.Bus = nil c.Check(svc.Start(), Equals, ErrNotConfigured) } func (ss *serviceSuite) TestStartFailsOnBusDialFailure(c *C) { bus := testibus.NewTestingEndpoint(condition.Work(false), nil) svc := NewPushService(testSetup, ss.log) svc.Bus = bus c.Check(svc.Start(), ErrorMatches, `.*(?i)cond said no.*`) svc.Stop() } func (ss *serviceSuite) TestStartGrabsName(c *C) { svc := NewPushService(testSetup, ss.log) svc.Bus = ss.bus c.Assert(svc.Start(), IsNil) callArgs := testibus.GetCallArgs(ss.bus) defer svc.Stop() c.Assert(callArgs, NotNil) c.Check(callArgs[0].Member, Equals, "::GrabName") } func (ss *serviceSuite) TestStopClosesBus(c *C) { svc := NewPushService(testSetup, ss.log) svc.Bus = ss.bus c.Assert(svc.Start(), IsNil) svc.Stop() callArgs := testibus.GetCallArgs(ss.bus) c.Assert(callArgs, NotNil) c.Check(callArgs[len(callArgs)-1].Member, Equals, "::Close") } // registration tests func (ss *serviceSuite) TestGetRegAuthWorks(c *C) { ch := make(chan string, 1) setup := &PushServiceSetup{ RegURL: helpers.ParseURL("http://foo"), AuthGetter: func(s string) string { ch <- s return "Auth " + s }, } svc := NewPushService(setup, ss.log) svc.Bus = ss.bus url, auth := svc.getAuthorization("/op") c.Check(auth, Equals, "Auth http://foo/op") c.Assert(len(ch), Equals, 1) c.Check(<-ch, Equals, "http://foo/op") c.Check(url, Equals, "http://foo/op") } func (ss *serviceSuite) TestGetRegAuthDoesNotPanic(c *C) { svc := NewPushService(testSetup, ss.log) svc.Bus = ss.bus _, auth := svc.getAuthorization("/op") c.Check(auth, Equals, "") } func (ss *serviceSuite) TestRegistrationAndUnregistrationFailIfBadArgs(c *C) { for i, s := range []struct { args []interface{} errt error }{ {nil, ErrBadArgCount}, {[]interface{}{}, ErrBadArgCount}, {[]interface{}{1}, ErrBadArgType}, {[]interface{}{"foo"}, click.ErrInvalidAppId}, {[]interface{}{"foo", "bar"}, ErrBadArgCount}, } { reg, err := new(PushService).register("/bar", s.args, nil) c.Check(reg, IsNil, Commentf("iteration #%d", i)) c.Check(err, Equals, s.errt, Commentf("iteration #%d", i)) reg, err = new(PushService).unregister("/bar", s.args, nil) c.Check(reg, IsNil, Commentf("iteration #%d", i)) c.Check(err, Equals, s.errt, Commentf("iteration #%d", i)) } } func (ss *serviceSuite) TestRegistrationWorks(c *C) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buf := make([]byte, 256) n := r.ContentLength _, e := io.ReadFull(r.Body, buf[:n]) c.Assert(e, IsNil) req := registrationRequest{} c.Assert(json.Unmarshal(buf[:n], &req), IsNil) c.Check(req, DeepEquals, registrationRequest{"fake-device-id", anAppId}) c.Check(r.URL.Path, Equals, "/register") w.Header().Set("Content-Type", "application/json") fmt.Fprintln(w, `{"ok":true,"token":"blob-of-bytes"}`) })) defer ts.Close() setup := &PushServiceSetup{ DeviceId: "fake-device-id", RegURL: helpers.ParseURL(ts.URL), AuthGetter: func(string) string { return "tok" }, } svc := NewPushService(setup, ss.log) svc.Bus = ss.bus // this'll check (un)quoting, too reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil) c.Assert(err, IsNil) c.Assert(reg, HasLen, 1) regs, ok := reg[0].(string) c.Check(ok, Equals, true) c.Check(regs, Equals, "blob-of-bytes") } func (ss *serviceSuite) TestRegistrationOverrideWorks(c *C) { envar := "PUSH_REG_" + string(nih.Quote([]byte(anAppId))) os.Setenv(envar, "42") defer os.Setenv(envar, "") reg, err := new(PushService).register(aPackageOnBus, []interface{}{anAppId}, nil) c.Assert(reg, HasLen, 1) regs, ok := reg[0].(string) c.Check(ok, Equals, true) c.Check(regs, Equals, "42") c.Check(err, IsNil) } func (ss *serviceSuite) TestManageRegFailsOnBadAuth(c *C) { // ... no auth added svc := NewPushService(testSetup, ss.log) svc.Bus = ss.bus reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil) c.Check(reg, IsNil) c.Check(err, Equals, ErrBadAuth) } func (ss *serviceSuite) TestManageRegFailsOnNoServer(c *C) { setup := &PushServiceSetup{ DeviceId: "fake-device-id", RegURL: helpers.ParseURL("xyzzy://"), AuthGetter: func(string) string { return "tok" }, } svc := NewPushService(setup, ss.log) svc.Bus = ss.bus reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil) c.Check(reg, IsNil) c.Check(err, ErrorMatches, "unable to request registration: .*") } func (ss *serviceSuite) TestManageRegFailsOn401(c *C) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "Unauthorized", 401) })) defer ts.Close() setup := &PushServiceSetup{ DeviceId: "fake-device-id", RegURL: helpers.ParseURL(ts.URL), AuthGetter: func(string) string { return "tok" }, } svc := NewPushService(setup, ss.log) svc.Bus = ss.bus reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil) c.Check(err, Equals, ErrBadAuth) c.Check(reg, IsNil) } func (ss *serviceSuite) TestManageRegFailsOn40x(c *C) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "I'm a teapot", 418) })) defer ts.Close() setup := &PushServiceSetup{ DeviceId: "fake-device-id", RegURL: helpers.ParseURL(ts.URL), AuthGetter: func(string) string { return "tok" }, } svc := NewPushService(setup, ss.log) svc.Bus = ss.bus reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil) c.Check(err, Equals, ErrBadRequest) c.Check(reg, IsNil) } func (ss *serviceSuite) TestManageRegFailsOn50x(c *C) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not implemented", 501) })) defer ts.Close() setup := &PushServiceSetup{ DeviceId: "fake-device-id", RegURL: helpers.ParseURL(ts.URL), AuthGetter: func(string) string { return "tok" }, } svc := NewPushService(setup, ss.log) svc.Bus = ss.bus reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil) c.Check(err, Equals, ErrBadServer) c.Check(reg, IsNil) } func (ss *serviceSuite) TestManageRegFailsOnBadJSON(c *C) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buf := make([]byte, 256) n := r.ContentLength _, e := io.ReadFull(r.Body, buf[:n]) c.Assert(e, IsNil) req := registrationRequest{} c.Assert(json.Unmarshal(buf[:n], &req), IsNil) c.Check(req, DeepEquals, registrationRequest{"fake-device-id", anAppId}) w.Header().Set("Content-Type", "application/json") fmt.Fprintln(w, `{`) })) defer ts.Close() setup := &PushServiceSetup{ DeviceId: "fake-device-id", RegURL: helpers.ParseURL(ts.URL), AuthGetter: func(string) string { return "tok" }, } svc := NewPushService(setup, ss.log) svc.Bus = ss.bus // this'll check (un)quoting, too reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil) c.Check(reg, IsNil) c.Check(err, ErrorMatches, "unable to unmarshal register response: .*") } func (ss *serviceSuite) TestManageRegFailsOnBadJSONDocument(c *C) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buf := make([]byte, 256) n := r.ContentLength _, e := io.ReadFull(r.Body, buf[:n]) c.Assert(e, IsNil) req := registrationRequest{} c.Assert(json.Unmarshal(buf[:n], &req), IsNil) c.Check(req, DeepEquals, registrationRequest{"fake-device-id", anAppId}) w.Header().Set("Content-Type", "application/json") fmt.Fprintln(w, `{"bananas": "very yes"}`) })) defer ts.Close() setup := &PushServiceSetup{ DeviceId: "fake-device-id", RegURL: helpers.ParseURL(ts.URL), AuthGetter: func(string) string { return "tok" }, } svc := NewPushService(setup, ss.log) svc.Bus = ss.bus // this'll check (un)quoting, too reg, err := svc.register(aPackageOnBus, []interface{}{anAppId}, nil) c.Check(reg, IsNil) c.Check(err, Equals, ErrBadToken) } func (ss *serviceSuite) TestDBusUnregisterWorks(c *C) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buf := make([]byte, 256) n := r.ContentLength _, e := io.ReadFull(r.Body, buf[:n]) c.Assert(e, IsNil) req := registrationRequest{} c.Assert(json.Unmarshal(buf[:n], &req), IsNil) c.Check(req, DeepEquals, registrationRequest{"fake-device-id", anAppId}) c.Check(r.URL.Path, Equals, "/unregister") w.Header().Set("Content-Type", "application/json") fmt.Fprintln(w, `{"ok":true,"token":"blob-of-bytes"}`) })) defer ts.Close() setup := &PushServiceSetup{ DeviceId: "fake-device-id", RegURL: helpers.ParseURL(ts.URL), AuthGetter: func(string) string { return "tok" }, } svc := NewPushService(setup, ss.log) svc.Bus = ss.bus // this'll check (un)quoting, too reg, err := svc.unregister(aPackageOnBus, []interface{}{anAppId}, nil) c.Assert(err, IsNil) c.Assert(reg, HasLen, 0) } func (ss *serviceSuite) TestUnregistrationWorks(c *C) { invoked := make(chan bool, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buf := make([]byte, 256) n := r.ContentLength _, e := io.ReadFull(r.Body, buf[:n]) c.Assert(e, IsNil) req := registrationRequest{} c.Assert(json.Unmarshal(buf[:n], &req), IsNil) c.Check(req, DeepEquals, registrationRequest{"fake-device-id", anAppId}) c.Check(r.URL.Path, Equals, "/unregister") invoked <- true w.Header().Set("Content-Type", "application/json") fmt.Fprintln(w, `{"ok":true}`) })) defer ts.Close() setup := &PushServiceSetup{ DeviceId: "fake-device-id", RegURL: helpers.ParseURL(ts.URL), AuthGetter: func(string) string { return "tok" }, } svc := NewPushService(setup, ss.log) svc.Bus = ss.bus err := svc.Unregister(anAppId) c.Assert(err, IsNil) c.Check(invoked, HasLen, 1) } ubuntu-push-0.68+16.04.20160310.2/client/service/postal_test.go0000644000015600001650000007403512670364255024274 0ustar pbuserpbgroup00000000000000/* 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 service import ( "encoding/json" "errors" "fmt" "io/ioutil" "os" "path/filepath" "sort" "sync" "time" "launchpad.net/go-dbus/v1" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/bus/notifications" testibus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/bus/windowstack" "launchpad.net/ubuntu-push/click" clickhelp "launchpad.net/ubuntu-push/click/testing" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/messaging/reply" "launchpad.net/ubuntu-push/nih" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" ) // takeNext takes a value from given channel with a 5s timeout func takeNextBool(ch <-chan bool) bool { select { case <-time.After(5 * time.Second): panic("channel stuck: too long waiting") case v := <-ch: return v } } // takeNextBytes takes a value from given channel with a 5s timeout func takeNextBytes(ch <-chan []byte) []byte { select { case <-time.After(5 * time.Second): panic("channel stuck: too long waiting") case v := <-ch: return v } } // takeNextHelperOutput takes a value from given channel with a 5s timeout func takeNextHelperOutput(ch <-chan *launch_helper.HelperOutput) *launch_helper.HelperOutput { select { case <-time.After(5 * time.Second): panic("channel stuck: too long waiting") case v := <-ch: return v } } func takeNextError(ch <-chan error) error { select { case <-time.After(5 * time.Second): panic("channel stuck: too long waiting") case v := <-ch: return v } } func installTickMessageHandler(svc *PostalService) chan bool { ch := make(chan bool) msgHandler := svc.GetMessageHandler() svc.SetMessageHandler(func(app *click.AppId, nid string, output *launch_helper.HelperOutput) bool { var b bool if msgHandler != nil { b = msgHandler(app, nid, output) } ch <- b return b }) return ch } type fakeHelperLauncher struct { i int ch chan []byte done func(string) } func (fhl *fakeHelperLauncher) InstallObserver(done func(string)) error { fhl.done = done return nil } func (fhl *fakeHelperLauncher) RemoveObserver() error { return nil } func (fhl *fakeHelperLauncher) Stop(_, _ string) error { return nil } func (fhl *fakeHelperLauncher) HelperInfo(app *click.AppId) (string, string) { if app.Click { return "helpId", "bar" } else { return "", "lhex" } } func (fhl *fakeHelperLauncher) Launch(_, _, f1, f2 string) (string, error) { dat, err := ioutil.ReadFile(f1) if err != nil { return "", err } err = ioutil.WriteFile(f2, dat, os.ModeTemporary) if err != nil { return "", err } id := []string{"0", "1", "2"}[fhl.i] fhl.i++ fhl.ch <- dat return id, nil } type fakeUrlDispatcher struct { DispatchDone chan bool DispatchCalls [][]string TestURLCalls []map[string][]string NextTestURLResult bool DispatchShouldFail bool Lock sync.Mutex } func (fud *fakeUrlDispatcher) DispatchURL(url string, app *click.AppId) error { fud.Lock.Lock() defer fud.Lock.Unlock() fud.DispatchCalls = append(fud.DispatchCalls, []string{url, app.DispatchPackage()}) if fud.DispatchShouldFail { return errors.New("fail!") } fud.DispatchDone <- true return nil } func (fud *fakeUrlDispatcher) TestURL(app *click.AppId, urls []string) bool { fud.Lock.Lock() defer fud.Lock.Unlock() var args = make(map[string][]string, 1) args[app.DispatchPackage()] = urls fud.TestURLCalls = append(fud.TestURLCalls, args) return fud.NextTestURLResult } type postalSuite struct { log *helpers.TestLogger cfg *PostalServiceSetup bus bus.Endpoint notifBus bus.Endpoint counterBus bus.Endpoint hapticBus bus.Endpoint unityGreeterBus bus.Endpoint winStackBus bus.Endpoint accountsBus bus.Endpoint accountsCh chan []interface{} fakeLauncher *fakeHelperLauncher getTempDir func(string) (string, error) oldIsBlisted func(*click.AppId) bool blacklisted bool } type ualPostalSuite struct { postalSuite } type trivialPostalSuite struct { postalSuite } var _ = Suite(&ualPostalSuite{}) var _ = Suite(&trivialPostalSuite{}) func (ps *postalSuite) SetUpTest(c *C) { ps.oldIsBlisted = isBlacklisted isBlacklisted = func(*click.AppId) bool { return ps.blacklisted } ps.log = helpers.NewTestLogger(c, "debug") ps.cfg = &PostalServiceSetup{} ps.bus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true)) ps.notifBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true)) ps.counterBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true)) ps.accountsBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), map[string]dbus.Variant{ "IncomingMessageVibrate": dbus.Variant{true}, "SilentMode": dbus.Variant{false}, "IncomingMessageSound": dbus.Variant{""}, "IncomingMessageVibrateSilentMode": dbus.Variant{false}, }) ps.hapticBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true)) ps.unityGreeterBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), false) ps.winStackBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), []windowstack.WindowsInfo{}) ps.fakeLauncher = &fakeHelperLauncher{ch: make(chan []byte)} ps.blacklisted = false ps.getTempDir = launch_helper.GetTempDir d := c.MkDir() launch_helper.GetTempDir = func(pkgName string) (string, error) { tmpDir := filepath.Join(d, pkgName) return tmpDir, os.MkdirAll(tmpDir, 0700) } ps.accountsCh = make(chan []interface{}) testibus.SetWatchSource(ps.accountsBus, "PropertiesChanged", ps.accountsCh) } func (ps *postalSuite) TearDownTest(c *C) { isBlacklisted = ps.oldIsBlisted launch_helper.GetTempDir = ps.getTempDir close(ps.accountsCh) } func (ts *trivialPostalSuite) SetUpTest(c *C) { ts.postalSuite.SetUpTest(c) useTrivialHelper = true } func (ts *trivialPostalSuite) TearDownTest(c *C) { ts.postalSuite.TearDownTest(c) useTrivialHelper = false } func (ps *postalSuite) replaceBuses(pst *PostalService) *PostalService { pst.Bus = ps.bus pst.NotificationsEndp = ps.notifBus pst.EmblemCounterEndp = ps.counterBus pst.AccountsEndp = ps.accountsBus pst.HapticEndp = ps.hapticBus pst.UnityGreeterEndp = ps.unityGreeterBus pst.WindowStackEndp = ps.winStackBus pst.launchers = map[string]launch_helper.HelperLauncher{} return pst } func (ps *postalSuite) TestStart(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) c.Check(svc.IsRunning(), Equals, false) c.Check(svc.Start(), IsNil) c.Check(svc.IsRunning(), Equals, true) svc.Stop() } func (ps *postalSuite) TestStartTwice(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) c.Check(svc.Start(), IsNil) c.Check(svc.Start(), Equals, ErrAlreadyStarted) svc.Stop() } func (ps *postalSuite) TestStartNoLog(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, nil)) c.Check(svc.Start(), Equals, ErrNotConfigured) } func (ps *postalSuite) TestStartNoBus(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.Bus = nil c.Check(svc.Start(), Equals, ErrNotConfigured) svc = ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.NotificationsEndp = nil c.Check(svc.Start(), Equals, ErrNotConfigured) } func (ps *postalSuite) TestTakeTheBusFail(c *C) { nEndp := testibus.NewMultiValuedTestingEndpoint(condition.Work(true), condition.Work(false)) svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.NotificationsEndp = nEndp _, err := svc.takeTheBus() c.Check(err, NotNil) } func (ps *postalSuite) TestTakeTheBusOk(c *C) { nEndp := testibus.NewMultiValuedTestingEndpoint(condition.Work(true), condition.Work(true), []interface{}{uint32(1), "hello"}) svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.NotificationsEndp = nEndp _, err := svc.takeTheBus() c.Check(err, IsNil) } func (ps *postalSuite) TestStartFailsOnBusDialFailure(c *C) { // XXX actually, we probably want to autoredial this svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.Bus = testibus.NewTestingEndpoint(condition.Work(false), nil) c.Check(svc.Start(), ErrorMatches, `.*(?i)cond said no.*`) svc.Stop() } func (ps *postalSuite) TestStartGrabsName(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) c.Assert(svc.Start(), IsNil) callArgs := testibus.GetCallArgs(ps.bus) defer svc.Stop() c.Assert(callArgs, NotNil) c.Check(callArgs[0].Member, Equals, "::GrabName") } func (ps *postalSuite) TestStopClosesBus(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) c.Assert(svc.Start(), IsNil) svc.Stop() callArgs := testibus.GetCallArgs(ps.bus) c.Assert(callArgs, NotNil) c.Check(callArgs[len(callArgs)-1].Member, Equals, "::Close") } // // post() tests func (ps *postalSuite) TestPostHappyPath(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.msgHandler = nil ch := installTickMessageHandler(svc) svc.launchers = map[string]launch_helper.HelperLauncher{ "click": ps.fakeLauncher, } c.Assert(svc.Start(), IsNil) payload := `{"message": {"world":1}}` rvs, err := svc.post(aPackageOnBus, []interface{}{anAppId, payload}, nil) c.Assert(err, IsNil) c.Check(rvs, IsNil) if ps.fakeLauncher.done != nil { // wait for the two posts to "launch" inputData := takeNextBytes(ps.fakeLauncher.ch) c.Check(string(inputData), Equals, payload) go ps.fakeLauncher.done("0") // OneDone } c.Check(takeNextBool(ch), Equals, false) // one, // xxx here? c.Assert(svc.mbox, HasLen, 1) box, ok := svc.mbox[anAppId] c.Check(ok, Equals, true) msgs := box.AllMessages() c.Assert(msgs, HasLen, 1) c.Check(msgs[0], Equals, `{"world":1}`) c.Check(box.nids[0], Not(Equals), "") } func (ps *postalSuite) TestPostFailsIfBadArgs(c *C) { for i, s := range []struct { args []interface{} errt error }{ {nil, ErrBadArgCount}, {[]interface{}{}, ErrBadArgCount}, {[]interface{}{1}, ErrBadArgCount}, {[]interface{}{anAppId, 1}, ErrBadArgType}, {[]interface{}{anAppId, "zoom"}, ErrBadJSON}, {[]interface{}{1, "hello"}, ErrBadArgType}, {[]interface{}{1, 2, 3}, ErrBadArgCount}, {[]interface{}{"bar", "hello"}, click.ErrInvalidAppId}, } { reg, err := new(PostalService).post(aPackageOnBus, s.args, nil) c.Check(reg, IsNil, Commentf("iteration #%d", i)) c.Check(err, Equals, s.errt, Commentf("iteration #%d", i)) } } // Post() tests func (ps *postalSuite) TestPostWorks(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.msgHandler = nil ch := installTickMessageHandler(svc) fakeLauncher2 := &fakeHelperLauncher{ch: make(chan []byte)} svc.launchers = map[string]launch_helper.HelperLauncher{ "click": ps.fakeLauncher, "legacy": fakeLauncher2, } c.Assert(svc.Start(), IsNil) app := clickhelp.MustParseAppId(anAppId) // these two, being for the same app, will be done sequentially. svc.Post(app, "m1", json.RawMessage(`{"message":{"world":1}}`)) svc.Post(app, "m2", json.RawMessage(`{"message":{"moon":1}}`)) classicApp := clickhelp.MustParseAppId("_classic-app") svc.Post(classicApp, "m3", json.RawMessage(`{"message":{"mars":42}}`)) if ps.fakeLauncher.done != nil { // wait for the two posts to "launch" takeNextBytes(ps.fakeLauncher.ch) takeNextBytes(fakeLauncher2.ch) go ps.fakeLauncher.done("0") // OneDone go fakeLauncher2.done("0") inputData := takeNextBytes(ps.fakeLauncher.ch) c.Check(string(inputData), Equals, `{"message":{"moon":1}}`) go ps.fakeLauncher.done("1") // OneDone } c.Check(takeNextBool(ch), Equals, false) // one, c.Check(takeNextBool(ch), Equals, false) // two, c.Check(takeNextBool(ch), Equals, false) // three posts c.Assert(svc.mbox, HasLen, 2) box, ok := svc.mbox[anAppId] c.Check(ok, Equals, true) msgs := box.AllMessages() c.Assert(msgs, HasLen, 2) c.Check(msgs[0], Equals, `{"world":1}`) c.Check(msgs[1], Equals, `{"moon":1}`) c.Check(box.nids, DeepEquals, []string{"m1", "m2"}) box, ok = svc.mbox["_classic-app"] c.Assert(ok, Equals, true) msgs = box.AllMessages() c.Assert(msgs, HasLen, 1) c.Check(msgs[0], Equals, `{"mars":42}`) } func (ps *postalSuite) TestPostCallsMessageHandlerDetails(c *C) { ch := make(chan *launch_helper.HelperOutput) svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.launchers = map[string]launch_helper.HelperLauncher{ "click": ps.fakeLauncher, } c.Assert(svc.Start(), IsNil) // check the message handler gets called app := clickhelp.MustParseAppId(anAppId) f := func(app *click.AppId, nid string, s *launch_helper.HelperOutput) bool { c.Check(app.Base(), Equals, anAppId) c.Check(nid, Equals, "m7") ch <- s return true } svc.SetMessageHandler(f) svc.Post(app, "m7", json.RawMessage("{}")) if ps.fakeLauncher.done != nil { takeNextBytes(ps.fakeLauncher.ch) go ps.fakeLauncher.done("0") // OneDone } c.Check(takeNextHelperOutput(ch), DeepEquals, &launch_helper.HelperOutput{}) } func (ps *postalSuite) TestAfterMessageHandlerSignal(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.msgHandler = nil hInp := &launch_helper.HelperInput{ App: clickhelp.MustParseAppId(anAppId), } res := &launch_helper.HelperResult{Input: hInp} svc.handleHelperResult(res) // and check it fired the right signal callArgs := testibus.GetCallArgs(ps.bus) l := len(callArgs) if l < 1 { c.Fatal("not enough elements in resposne from GetCallArgs") } c.Check(callArgs[l-1].Member, Equals, "::Signal") c.Check(callArgs[l-1].Args, DeepEquals, []interface{}{"Post", aPackageOnBus, []interface{}{anAppId}}) } func (ps *postalSuite) TestFailingMessageHandlerSurvived(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.SetMessageHandler(func(*click.AppId, string, *launch_helper.HelperOutput) bool { return false }) hInp := &launch_helper.HelperInput{ App: clickhelp.MustParseAppId(anAppId), } res := &launch_helper.HelperResult{Input: hInp} svc.handleHelperResult(res) c.Check(ps.log.Captured(), Equals, "DEBUG msgHandler did not present the notification\n") // we actually want to send a signal even if we didn't do anything callArgs := testibus.GetCallArgs(ps.bus) c.Assert(len(callArgs), Equals, 1) c.Check(callArgs[0].Member, Equals, "::Signal") } // // Notifications tests func (ps *postalSuite) TestNotificationsWorks(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) nots, err := svc.popAll(aPackageOnBus, []interface{}{anAppId}, nil) c.Assert(err, IsNil) c.Assert(nots, NotNil) c.Assert(nots, HasLen, 1) c.Check(nots[0], HasLen, 0) c.Assert(svc.mbox, IsNil) svc.mbox = make(map[string]*mBox) nots, err = svc.popAll(aPackageOnBus, []interface{}{anAppId}, nil) c.Assert(err, IsNil) c.Assert(nots, NotNil) c.Assert(nots, HasLen, 1) c.Check(nots[0], HasLen, 0) box := new(mBox) svc.mbox[anAppId] = box m1 := json.RawMessage(`"m1"`) m2 := json.RawMessage(`"m2"`) box.Append(m1, "n1") box.Append(m2, "n2") nots, err = svc.popAll(aPackageOnBus, []interface{}{anAppId}, nil) c.Assert(err, IsNil) c.Assert(nots, NotNil) c.Assert(nots, HasLen, 1) c.Check(nots[0], DeepEquals, []string{string(m1), string(m2)}) } func (ps *postalSuite) TestNotificationsFailsIfBadArgs(c *C) { for i, s := range []struct { args []interface{} errt error }{ {nil, ErrBadArgCount}, {[]interface{}{}, ErrBadArgCount}, {[]interface{}{1}, ErrBadArgType}, {[]interface{}{"potato"}, click.ErrInvalidAppId}, } { reg, err := new(PostalService).popAll(aPackageOnBus, s.args, nil) c.Check(reg, IsNil, Commentf("iteration #%d", i)) c.Check(err, Equals, s.errt, Commentf("iteration #%d", i)) } } func (ps *postalSuite) TestMessageHandlerPublicAPI(c *C) { svc := new(PostalService) c.Assert(svc.msgHandler, IsNil) var ext = &launch_helper.HelperOutput{} f := func(_ *click.AppId, _ string, s *launch_helper.HelperOutput) bool { ext = s; return false } c.Check(svc.GetMessageHandler(), IsNil) svc.SetMessageHandler(f) c.Check(svc.GetMessageHandler(), NotNil) hOutput := &launch_helper.HelperOutput{[]byte("37"), nil} c.Check(svc.msgHandler(nil, "", hOutput), Equals, false) c.Check(ext, DeepEquals, hOutput) } func (ps *postalSuite) TestMessageHandlerPresents(c *C) { endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(1)) svc := NewPostalService(ps.cfg, ps.log) svc.Bus = endp svc.EmblemCounterEndp = endp svc.AccountsEndp = ps.accountsBus svc.HapticEndp = endp svc.NotificationsEndp = endp svc.UnityGreeterEndp = ps.unityGreeterBus svc.WindowStackEndp = ps.winStackBus nopTicker := make(chan []interface{}) testibus.SetWatchSource(endp, "ActionInvoked", nopTicker) defer close(nopTicker) svc.launchers = map[string]launch_helper.HelperLauncher{} svc.fallbackVibration = &launch_helper.Vibration{Pattern: []uint32{1}} c.Assert(svc.Start(), IsNil) // Persist is false so we just check the log card := &launch_helper.Card{Icon: "icon-value", Summary: "summary-value", Body: "body-value", Popup: true, Persist: false} vib := json.RawMessage(`true`) emb := &launch_helper.EmblemCounter{Count: 2, Visible: true} output := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{Card: card, EmblemCounter: emb, RawVibration: vib}} b := svc.messageHandler(&click.AppId{}, "", output) c.Assert(b, Equals, true) args := testibus.GetCallArgs(endp) l := len(args) if l < 4 { c.Fatal("not enough elements in response from GetCallArgs") } mm := make([]string, 4) for i, m := range args[l-4:] { mm[i] = m.Member } sort.Strings(mm) // check the Present() methods were called. // For dbus-backed presenters, just check the right dbus methods are called c.Check(mm, DeepEquals, []string{"::SetProperty", "::SetProperty", "Notify", "VibratePattern"}) // For the other ones, check the logs c.Check(ps.log.Captured(), Matches, `(?sm).* no persistable card:.*`) c.Check(ps.log.Captured(), Matches, `(?sm).* notification has no Sound:.*`) } func (ps *postalSuite) TestMessageHandlerReportsFailedNotifies(c *C) { endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), 1) nopTicker := make(chan []interface{}) testibus.SetWatchSource(endp, "ActionInvoked", nopTicker) defer close(nopTicker) svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.NotificationsEndp = endp c.Assert(svc.Start(), IsNil) card := &launch_helper.Card{Icon: "icon-value", Summary: "summary-value", Body: "body-value", Popup: true} notif := &launch_helper.Notification{Card: card} output := &launch_helper.HelperOutput{Notification: notif} b := svc.messageHandler(&click.AppId{}, "", output) c.Check(b, Equals, false) } func (ps *postalSuite) TestMessageHandlerInhibition(c *C) { endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), []windowstack.WindowsInfo{{0, "com.example.test_test-app", true, 0}}) svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.WindowStackEndp = endp c.Assert(svc.Start(), IsNil) output := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{}} // Doesn't matter b := svc.messageHandler(clickhelp.MustParseAppId("com.example.test_test-app_0"), "", output) c.Check(b, Equals, false) c.Check(ps.log.Captured(), Matches, `(?sm).* notification skipped because app is focused.*`) } func (ps *postalSuite) TestMessageHandlerReportsButIgnoresUnmarshalErrors(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) c.Assert(svc.Start(), IsNil) output := &launch_helper.HelperOutput{[]byte(`broken`), nil} b := svc.messageHandler(nil, "", output) c.Check(b, Equals, false) c.Check(ps.log.Captured(), Matches, "(?msi).*skipping notification: nil.*") } func (ps *postalSuite) TestMessageHandlerReportsButIgnoresNilNotifies(c *C) { endp := testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false)) svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) c.Assert(svc.Start(), IsNil) svc.NotificationsEndp = endp output := &launch_helper.HelperOutput{[]byte(`{}`), nil} b := svc.messageHandler(nil, "", output) c.Assert(b, Equals, false) c.Check(ps.log.Captured(), Matches, "(?msi).*skipping notification: nil.*") } func (ps *postalSuite) TestMessageHandlerInvalidAction(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) c.Assert(svc.Start(), IsNil) fakeDisp := new(fakeUrlDispatcher) svc.urlDispatcher = fakeDisp fakeDisp.NextTestURLResult = false card := launch_helper.Card{Actions: []string{"notsupported://test-app"}} output := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{Card: &card}} appId := clickhelp.MustParseAppId("com.example.test_test-app_0") b := svc.messageHandler(appId, "", output) c.Check(b, Equals, false) fakeDisp.Lock.Lock() defer fakeDisp.Lock.Unlock() c.Assert(len(fakeDisp.DispatchCalls), Equals, 0) c.Assert(len(fakeDisp.TestURLCalls), Equals, 1) c.Assert(fakeDisp.TestURLCalls[0][appId.DispatchPackage()], DeepEquals, []string{"notsupported://test-app"}) } func (ps *postalSuite) TestHandleActionsDispatches(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) fmm := new(fakeMM) app, _ := click.ParseAppId("com.example.test_test-app") c.Assert(svc.Start(), IsNil) fakeDisp := new(fakeUrlDispatcher) fakeDisp.DispatchDone = make(chan bool) svc.urlDispatcher = fakeDisp fakeDisp.NextTestURLResult = true svc.messagingMenu = fmm aCh := make(chan *notifications.RawAction) rCh := make(chan *reply.MMActionReply) go func() { aCh <- nil // just in case? aCh <- ¬ifications.RawAction{App: app, Action: "potato://", Nid: "xyzzy"} close(aCh) }() go svc.handleActions(aCh, rCh) takeNextBool(fakeDisp.DispatchDone) fakeDisp.Lock.Lock() defer fakeDisp.Lock.Unlock() c.Assert(len(fakeDisp.DispatchCalls), Equals, 1) c.Assert(fakeDisp.DispatchCalls[0][0], Equals, "potato://") c.Assert(fakeDisp.DispatchCalls[0][1], Equals, app.DispatchPackage()) c.Check(fmm.calls, DeepEquals, []string{"remove:xyzzy:true"}) } func (ps *postalSuite) TestHandleMMUActionsDispatches(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) c.Assert(svc.Start(), IsNil) fakeDisp := new(fakeUrlDispatcher) svc.urlDispatcher = fakeDisp fakeDisp.DispatchDone = make(chan bool) fakeDisp.NextTestURLResult = true app, _ := click.ParseAppId("com.example.test_test-app") aCh := make(chan *notifications.RawAction) rCh := make(chan *reply.MMActionReply) go func() { rCh <- nil // just in case? rCh <- &reply.MMActionReply{App: app, Action: "potato://", Notification: "foo.bar"} close(rCh) }() go svc.handleActions(aCh, rCh) takeNextBool(fakeDisp.DispatchDone) fakeDisp.Lock.Lock() defer fakeDisp.Lock.Unlock() c.Assert(len(fakeDisp.DispatchCalls), Equals, 1) c.Assert(fakeDisp.DispatchCalls[0][0], Equals, "potato://") c.Assert(fakeDisp.DispatchCalls[0][1], Equals, app.DispatchPackage()) } func (ps *postalSuite) TestValidateActions(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) c.Assert(svc.Start(), IsNil) card := launch_helper.Card{Actions: []string{"potato://test-app"}} notif := &launch_helper.Notification{Card: &card} fakeDisp := new(fakeUrlDispatcher) svc.urlDispatcher = fakeDisp fakeDisp.NextTestURLResult = true b := svc.validateActions(clickhelp.MustParseAppId("com.example.test_test-app_0"), notif) c.Check(b, Equals, true) } func (ps *postalSuite) TestValidateActionsNoActions(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) card := launch_helper.Card{} notif := &launch_helper.Notification{Card: &card} b := svc.validateActions(clickhelp.MustParseAppId("com.example.test_test-app_0"), notif) c.Check(b, Equals, true) } func (ps *postalSuite) TestValidateActionsNoCard(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) notif := &launch_helper.Notification{} b := svc.validateActions(clickhelp.MustParseAppId("com.example.test_test-app_0"), notif) c.Check(b, Equals, true) } type fakeMM struct { calls []string } func (*fakeMM) Present(*click.AppId, string, *launch_helper.Notification) bool { return false } func (*fakeMM) GetCh() chan *reply.MMActionReply { return nil } func (*fakeMM) StartCleanupLoop() {} func (fmm *fakeMM) RemoveNotification(s string, b bool) { fmm.calls = append(fmm.calls, fmt.Sprintf("remove:%s:%t", s, b)) } func (fmm *fakeMM) Clear(*click.AppId, ...string) int { fmm.calls = append(fmm.calls, "clear") return 42 } func (fmm *fakeMM) Tags(*click.AppId) []string { fmm.calls = append(fmm.calls, "tags") return []string{"hello"} } func (ps *postalSuite) TestListPersistent(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) fmm := new(fakeMM) svc.messagingMenu = fmm itags, err := svc.listPersistent(aPackageOnBus, []interface{}{anAppId}, nil) c.Assert(err, IsNil) c.Assert(itags, HasLen, 1) c.Assert(itags[0], FitsTypeOf, []string(nil)) tags := itags[0].([]string) c.Check(tags, DeepEquals, []string{"hello"}) c.Check(fmm.calls, DeepEquals, []string{"tags"}) } func (ps *postalSuite) TestListPersistentErrors(c *C) { for i, s := range []struct { args []interface{} errt error }{ {nil, ErrBadArgCount}, {[]interface{}{}, ErrBadArgCount}, {[]interface{}{1}, ErrBadArgType}, {[]interface{}{anAppId, 2}, ErrBadArgCount}, {[]interface{}{"bar"}, click.ErrInvalidAppId}, } { reg, err := new(PostalService).listPersistent(aPackageOnBus, s.args, nil) c.Check(reg, IsNil, Commentf("iteration #%d", i)) c.Check(err, Equals, s.errt, Commentf("iteration #%d", i)) } } func (ps *postalSuite) TestClearPersistent(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) fmm := new(fakeMM) svc.messagingMenu = fmm icleared, err := svc.clearPersistent(aPackageOnBus, []interface{}{anAppId, "one", ""}, nil) c.Assert(err, IsNil) c.Assert(icleared, HasLen, 1) c.Check(icleared[0], Equals, uint32(42)) } func (ps *postalSuite) TestClearPersistentErrors(c *C) { for i, s := range []struct { args []interface{} err error }{ {[]interface{}{}, ErrBadArgCount}, {[]interface{}{42}, ErrBadArgType}, {[]interface{}{"xyzzy"}, click.ErrInvalidAppId}, {[]interface{}{anAppId, 42}, ErrBadArgType}, {[]interface{}{anAppId, "", 42}, ErrBadArgType}, } { _, err := new(PostalService).clearPersistent(aPackageOnBus, s.args, nil) c.Check(err, Equals, s.err, Commentf("iter %d", i)) } } func (ps *postalSuite) TestSetCounter(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) c.Check(svc.Start(), IsNil) _, err := svc.setCounter(aPackageOnBus, []interface{}{anAppId, int32(42), true}, nil) c.Assert(err, IsNil) quoted := "/" + string(nih.Quote([]byte(anAppId))) callArgs := testibus.GetCallArgs(svc.EmblemCounterEndp) c.Assert(callArgs, HasLen, 2) c.Check(callArgs[0].Member, Equals, "::SetProperty") c.Check(callArgs[0].Args, DeepEquals, []interface{}{"count", quoted, dbus.Variant{int32(42)}}) c.Check(callArgs[1].Member, Equals, "::SetProperty") c.Check(callArgs[1].Args, DeepEquals, []interface{}{"countVisible", quoted, dbus.Variant{true}}) } func (ps *postalSuite) TestSetCounterErrors(c *C) { svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.Start() for i, s := range []struct { args []interface{} err error }{ {[]interface{}{anAppId, int32(42), true}, nil}, // for reference {[]interface{}{}, ErrBadArgCount}, {[]interface{}{anAppId}, ErrBadArgCount}, {[]interface{}{anAppId, int32(42)}, ErrBadArgCount}, {[]interface{}{anAppId, int32(42), true, "potato"}, ErrBadArgCount}, {[]interface{}{"xyzzy", int32(42), true}, click.ErrInvalidAppId}, {[]interface{}{1234567, int32(42), true}, ErrBadArgType}, {[]interface{}{anAppId, "potatoe", true}, ErrBadArgType}, {[]interface{}{anAppId, int32(42), "ru"}, ErrBadArgType}, } { _, err := svc.setCounter(aPackageOnBus, s.args, nil) c.Check(err, Equals, s.err, Commentf("iter %d", i)) } } func (ps *postalSuite) TestBlacklisted(c *C) { ps.winStackBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), []windowstack.WindowsInfo{}, []windowstack.WindowsInfo{}, []windowstack.WindowsInfo{}, []windowstack.WindowsInfo{}) ps.unityGreeterBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), false, false, false, false) svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) svc.Start() ps.blacklisted = false emb := &launch_helper.EmblemCounter{Count: 2, Visible: true} card := &launch_helper.Card{Icon: "icon-value", Summary: "summary-value", Persist: true} output := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{Card: card}} embOut := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{EmblemCounter: emb}} app := clickhelp.MustParseAppId("com.example.app_app_1.0") // sanity check: things are presented as normal if blacklist == false ps.blacklisted = false c.Check(svc.messageHandler(app, "0", output), Equals, true) c.Check(svc.messageHandler(app, "1", embOut), Equals, true) ps.blacklisted = true // and regular notifications (but not emblem counters) are suppressed if blacklisted. c.Check(svc.messageHandler(app, "2", output), Equals, false) c.Check(svc.messageHandler(app, "3", embOut), Equals, true) } func (ps *postalSuite) TestFocusedAppButLockedScreenNotification(c *C) { appId := "com.example.test_test-app" ps.winStackBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), []windowstack.WindowsInfo{{0, appId, true, 0}}) ps.unityGreeterBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), true) svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log)) // svc.WindowStackEndp = ps.winStackBus svc.Start() card := &launch_helper.Card{Icon: "icon-value", Summary: "summary-value", Persist: true} output := &launch_helper.HelperOutput{Notification: &launch_helper.Notification{Card: card}} app := clickhelp.MustParseAppId(fmt.Sprintf("%v_0", appId)) c.Check(svc.messageHandler(app, "0", output), Equals, true) } ubuntu-push-0.68+16.04.20160310.2/client/service/common_test.go0000644000015600001650000000440112670364255024250 0ustar pbuserpbgroup00000000000000/* 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 service import ( . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/click" ) type commonSuite struct{} var _ = Suite(&commonSuite{}) func (cs *commonSuite) TestGrabDBusPackageAndAppIdWorks(c *C) { svc := new(DBusService) aDBusPath := "/com/ubuntu/Postal/com_2eexample_2etest" aPackage := "com.example.test" anAppId := aPackage + "_test" app, err := svc.grabDBusPackageAndAppId(aDBusPath, []interface{}{anAppId}, 0) c.Check(err, IsNil) c.Check(app.Package, Equals, aPackage) c.Check(app.Original(), Equals, anAppId) } type fakeInstalledChecker struct{} func (fakeInstalledChecker) Installed(app *click.AppId, setVersion bool) bool { return app.Original()[0] == 'c' } func (cs *commonSuite) TestGrabDBusPackageAndAppIdFails(c *C) { svc := new(DBusService) svc.installedChecker = fakeInstalledChecker{} aDBusPath := "/com/ubuntu/Postal/com_2eexample_2etest" aPackage := "com.example.test" anAppId := aPackage + "_test" for i, s := range []struct { path string args []interface{} numExtra int errt error }{ {aDBusPath, []interface{}{}, 0, ErrBadArgCount}, {aDBusPath, []interface{}{anAppId}, 1, ErrBadArgCount}, {aDBusPath, []interface{}{anAppId, anAppId}, 0, ErrBadArgCount}, {aDBusPath, []interface{}{1}, 0, ErrBadArgType}, {aDBusPath, []interface{}{aPackage}, 0, click.ErrInvalidAppId}, {aDBusPath, []interface{}{"x" + anAppId}, 0, click.ErrMissingApp}, {aDBusPath, []interface{}{"c" + anAppId}, 0, ErrAppIdMismatch}, } { comment := Commentf("iteration #%d", i) app, err := svc.grabDBusPackageAndAppId(s.path, s.args, s.numExtra) c.Check(err, Equals, s.errt, comment) c.Check(app, IsNil, comment) } } ubuntu-push-0.68+16.04.20160310.2/client/service/mbox.go0000644000015600001650000000412312670364255022667 0ustar pbuserpbgroup00000000000000/* 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 service import ( "encoding/json" ) var mBoxMaxMessagesSize = 128 * 1024 // mBox can hold a size-limited amount of notification messages for one application. type mBox struct { evicted int curSize int messages []string nids []string } func (box *mBox) evictFor(sz int) { evictedSize := 0 i := box.evicted n := len(box.messages) for evictedSize < sz && i < n { evictedSize += len(box.messages[i]) box.messages[i] = "" box.nids[i] = "" box.evicted++ i++ } box.curSize -= evictedSize } // Append appends a message with notification id to the mbox. func (box *mBox) Append(message json.RawMessage, nid string) { sz := len(message) if box.curSize+sz > mBoxMaxMessagesSize { // make space box.evictFor(sz) } n := len(box.messages) evicted := box.evicted if evicted > 0 { if evicted == n { // all evicted, just start from scratch box.messages = box.messages[0:0] box.nids = box.nids[0:0] box.evicted = 0 } else if evicted >= cap(box.messages)/2 { // amortize: do a copy only each cap/2 evicted copy(box.messages, box.messages[box.evicted:]) kept := n - box.evicted box.messages = box.messages[0:kept] copy(box.nids, box.nids[box.evicted:]) box.nids = box.nids[0:kept] box.evicted = 0 } } box.messages = append(box.messages, string(message)) box.nids = append(box.nids, nid) box.curSize += sz } // AllMessages gets all messages from the mbox. func (box *mBox) AllMessages() []string { return box.messages[box.evicted:] } ubuntu-push-0.68+16.04.20160310.2/client/service/postal.go0000644000015600001650000003126712670364255023235 0ustar pbuserpbgroup00000000000000/* 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 service import ( "encoding/json" "os" "sync" "code.google.com/p/go-uuid/uuid" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/bus/accounts" "launchpad.net/ubuntu-push/bus/emblemcounter" "launchpad.net/ubuntu-push/bus/haptic" "launchpad.net/ubuntu-push/bus/notifications" "launchpad.net/ubuntu-push/bus/unitygreeter" "launchpad.net/ubuntu-push/bus/windowstack" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/click/cblacklist" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/messaging" "launchpad.net/ubuntu-push/messaging/reply" "launchpad.net/ubuntu-push/nih" "launchpad.net/ubuntu-push/sounds" "launchpad.net/ubuntu-push/urldispatcher" "launchpad.net/ubuntu-push/util" ) type messageHandler func(*click.AppId, string, *launch_helper.HelperOutput) bool // a Presenter is something that knows how to present a Notification type Presenter interface { Present(*click.AppId, string, *launch_helper.Notification) bool } type notificationCentre interface { Presenter GetCh() chan *reply.MMActionReply RemoveNotification(string, bool) Tags(*click.AppId) []string Clear(*click.AppId, ...string) int } // PostalServiceSetup is a configuration object for the service type PostalServiceSetup struct { InstalledChecker click.InstalledChecker FallbackVibration *launch_helper.Vibration FallbackSound string } // PostalService is the dbus api type PostalService struct { DBusService mbox map[string]*mBox msgHandler messageHandler launchers map[string]launch_helper.HelperLauncher HelperPool launch_helper.HelperPool messagingMenu notificationCentre // the endpoints are only exposed for testing from client // XXX: uncouple some more so this isn't necessary EmblemCounterEndp bus.Endpoint AccountsEndp bus.Endpoint HapticEndp bus.Endpoint NotificationsEndp bus.Endpoint UnityGreeterEndp bus.Endpoint WindowStackEndp bus.Endpoint // presenters: Presenters []Presenter emblemCounter *emblemcounter.EmblemCounter accounts accounts.Accounts haptic *haptic.Haptic notifications *notifications.RawNotifications sound sounds.Sound // the url dispatcher, used for stuff. urlDispatcher urldispatcher.URLDispatcher unityGreeter *unitygreeter.UnityGreeter windowStack *windowstack.WindowStack // fallback values for simplified notification usage fallbackVibration *launch_helper.Vibration fallbackSound string } var ( PostalServiceBusAddress = bus.Address{ Interface: "com.ubuntu.Postal", Path: "/com/ubuntu/Postal", Name: "com.ubuntu.Postal", } ) var ( SystemUpdateUrl = "settings:///system/system-update" useTrivialHelper = os.Getenv("UBUNTU_PUSH_USE_TRIVIAL_HELPER") != "" ) // NewPostalService() builds a new service and returns it. func NewPostalService(setup *PostalServiceSetup, log logger.Logger) *PostalService { var svc = &PostalService{} svc.Log = log svc.Bus = bus.SessionBus.Endpoint(PostalServiceBusAddress, log) svc.installedChecker = setup.InstalledChecker svc.fallbackVibration = setup.FallbackVibration svc.fallbackSound = setup.FallbackSound svc.NotificationsEndp = bus.SessionBus.Endpoint(notifications.BusAddress, log) svc.EmblemCounterEndp = bus.SessionBus.Endpoint(emblemcounter.BusAddress, log) svc.AccountsEndp = bus.SystemBus.Endpoint(accounts.BusAddress, log) svc.HapticEndp = bus.SessionBus.Endpoint(haptic.BusAddress, log) svc.UnityGreeterEndp = bus.SessionBus.Endpoint(unitygreeter.BusAddress, log) svc.WindowStackEndp = bus.SessionBus.Endpoint(windowstack.BusAddress, log) svc.msgHandler = svc.messageHandler svc.launchers = launch_helper.DefaultLaunchers(log) return svc } // SetMessageHandler() sets the message-handling callback func (svc *PostalService) SetMessageHandler(callback messageHandler) { svc.lock.RLock() defer svc.lock.RUnlock() svc.msgHandler = callback } // GetMessageHandler() returns the (possibly nil) messaging handler callback func (svc *PostalService) GetMessageHandler() messageHandler { svc.lock.RLock() defer svc.lock.RUnlock() return svc.msgHandler } // Start() dials the bus, grab the name, and listens for method calls. func (svc *PostalService) Start() error { return svc.DBusService.Start(bus.DispatchMap{ "PopAll": svc.popAll, "Post": svc.post, "ListPersistent": svc.listPersistent, "ClearPersistent": svc.clearPersistent, "SetCounter": svc.setCounter, }, PostalServiceBusAddress, svc.init) } func (svc *PostalService) init() error { actionsCh, err := svc.takeTheBus() if err != nil { return err } svc.urlDispatcher = urldispatcher.New(svc.Log) svc.accounts = accounts.New(svc.AccountsEndp, svc.Log) err = svc.accounts.Start() if err != nil { return err } svc.sound = sounds.New(svc.Log, svc.accounts, svc.fallbackSound) svc.notifications = notifications.Raw(svc.NotificationsEndp, svc.Log, svc.sound) svc.emblemCounter = emblemcounter.New(svc.EmblemCounterEndp, svc.Log) svc.haptic = haptic.New(svc.HapticEndp, svc.Log, svc.accounts, svc.fallbackVibration) svc.messagingMenu = messaging.New(svc.Log) svc.Presenters = []Presenter{ svc.notifications, svc.emblemCounter, svc.haptic, svc.messagingMenu, } if useTrivialHelper { svc.HelperPool = launch_helper.NewTrivialHelperPool(svc.Log) } else { svc.HelperPool = launch_helper.NewHelperPool(svc.launchers, svc.Log) } svc.unityGreeter = unitygreeter.New(svc.UnityGreeterEndp, svc.Log) svc.windowStack = windowstack.New(svc.WindowStackEndp, svc.Log) go svc.consumeHelperResults(svc.HelperPool.Start()) go svc.handleActions(actionsCh, svc.messagingMenu.GetCh()) return nil } // xxx Stop() closing channels and helper launcher // handleactions loops on the actions channels waiting for actions and handling them func (svc *PostalService) handleActions(actionsCh <-chan *notifications.RawAction, mmuActionsCh <-chan *reply.MMActionReply) { Handle: for { select { case action, ok := <-actionsCh: if !ok { break Handle } if action == nil { svc.Log.Debugf("handleActions got nil action; ignoring") } else { url := action.Action // remove the notification from the messaging menu svc.messagingMenu.RemoveNotification(action.Nid, true) // this ignores the error (it's been logged already) svc.urlDispatcher.DispatchURL(url, action.App) } case mmuAction, ok := <-mmuActionsCh: if !ok { break Handle } if mmuAction == nil { svc.Log.Debugf("handleActions (MMU) got nil action; ignoring") } else { svc.Log.Debugf("handleActions (MMU) got: %v", mmuAction) url := mmuAction.Action // remove the notification from the messagingmenu map svc.messagingMenu.RemoveNotification(mmuAction.Notification, false) // this ignores the error (it's been logged already) svc.urlDispatcher.DispatchURL(url, mmuAction.App) } } } } func (svc *PostalService) takeTheBus() (<-chan *notifications.RawAction, error) { endps := []struct { name string endp bus.Endpoint }{ {"notifications", svc.NotificationsEndp}, {"emblemcounter", svc.EmblemCounterEndp}, {"accounts", svc.AccountsEndp}, {"haptic", svc.HapticEndp}, {"unitygreeter", svc.UnityGreeterEndp}, {"windowstack", svc.WindowStackEndp}, } for _, endp := range endps { if endp.endp == nil { svc.Log.Errorf("endpoint for %s is nil", endp.name) return nil, ErrNotConfigured } } var wg sync.WaitGroup wg.Add(len(endps)) for _, endp := range endps { go func(name string, endp bus.Endpoint) { util.NewAutoRedialer(endp).Redial() wg.Done() }(endp.name, endp.endp) } wg.Wait() return notifications.Raw(svc.NotificationsEndp, svc.Log, nil).WatchActions() } func (svc *PostalService) listPersistent(path string, args, _ []interface{}) ([]interface{}, error) { app, err := svc.grabDBusPackageAndAppId(path, args, 0) if err != nil { return nil, err } tagmap := svc.messagingMenu.Tags(app) return []interface{}{tagmap}, nil } func (svc *PostalService) clearPersistent(path string, args, _ []interface{}) ([]interface{}, error) { if len(args) == 0 { return nil, ErrBadArgCount } app, err := svc.grabDBusPackageAndAppId(path, args[:1], 0) if err != nil { return nil, err } tags := make([]string, len(args)-1) for i, itag := range args[1:] { tag, ok := itag.(string) if !ok { return nil, ErrBadArgType } tags[i] = tag } return []interface{}{uint32(svc.messagingMenu.Clear(app, tags...))}, nil } func (svc *PostalService) setCounter(path string, args, _ []interface{}) ([]interface{}, error) { app, err := svc.grabDBusPackageAndAppId(path, args, 2) if err != nil { return nil, err } count, ok := args[1].(int32) if !ok { return nil, ErrBadArgType } visible, ok := args[2].(bool) if !ok { return nil, ErrBadArgType } svc.emblemCounter.SetCounter(app, count, visible) return nil, nil } func (svc *PostalService) popAll(path string, args, _ []interface{}) ([]interface{}, error) { app, err := svc.grabDBusPackageAndAppId(path, args, 0) if err != nil { return nil, err } svc.lock.Lock() defer svc.lock.Unlock() if svc.mbox == nil { return []interface{}{[]string(nil)}, nil } appId := app.Original() box, ok := svc.mbox[appId] if !ok { return []interface{}{[]string(nil)}, nil } msgs := box.AllMessages() delete(svc.mbox, appId) return []interface{}{msgs}, nil } var newNid = uuid.New func (svc *PostalService) post(path string, args, _ []interface{}) ([]interface{}, error) { app, err := svc.grabDBusPackageAndAppId(path, args, 1) if err != nil { return nil, err } notif, ok := args[1].(string) if !ok { return nil, ErrBadArgType } var dummy interface{} rawJSON := json.RawMessage(notif) err = json.Unmarshal(rawJSON, &dummy) if err != nil { return nil, ErrBadJSON } svc.Post(app, "", rawJSON) return nil, nil } // Post() signals to an application over dbus that a notification // has arrived. If nid is "" generate one. func (svc *PostalService) Post(app *click.AppId, nid string, payload json.RawMessage) { if nid == "" { nid = newNid() } arg := launch_helper.HelperInput{ App: app, NotificationId: nid, Payload: payload, } var kind string if app.Click { kind = "click" } else { kind = "legacy" } svc.HelperPool.Run(kind, &arg) } func (svc *PostalService) consumeHelperResults(ch chan *launch_helper.HelperResult) { for res := range ch { svc.handleHelperResult(res) } } func (svc *PostalService) handleHelperResult(res *launch_helper.HelperResult) { svc.lock.Lock() defer svc.lock.Unlock() if svc.mbox == nil { svc.mbox = make(map[string]*mBox) } app := res.Input.App nid := res.Input.NotificationId output := res.HelperOutput appId := app.Original() box, ok := svc.mbox[appId] if !ok { box = new(mBox) svc.mbox[appId] = box } box.Append(output.Message, nid) if svc.msgHandler != nil { b := svc.msgHandler(app, nid, &output) if !b { svc.Log.Debugf("msgHandler did not present the notification") } } svc.Bus.Signal("Post", "/"+string(nih.Quote([]byte(app.Package))), []interface{}{appId}) } func (svc *PostalService) validateActions(app *click.AppId, notif *launch_helper.Notification) bool { if notif.Card == nil || len(notif.Card.Actions) == 0 { return true } return svc.urlDispatcher.TestURL(app, notif.Card.Actions) } var isBlacklisted = cblacklist.IsBlacklisted func (svc *PostalService) messageHandler(app *click.AppId, nid string, output *launch_helper.HelperOutput) bool { if output == nil || output.Notification == nil { svc.Log.Debugf("skipping notification: nil.") return false } // validate actions if !svc.validateActions(app, output.Notification) { // no need to log, (it's been logged already) return false } locked := svc.unityGreeter.IsActive() focused := svc.windowStack.IsAppFocused(app) if !locked && focused { svc.Log.Debugf("notification skipped because app is focused.") return false } if isBlacklisted(app) { svc.Log.Debugf("notification skipped (except emblem counter) because app is blacklisted") return svc.emblemCounter.Present(app, nid, output.Notification) } b := false for _, p := range svc.Presenters { // we don't want this to shortcut :) b = p.Present(app, nid, output.Notification) || b } return b } ubuntu-push-0.68+16.04.20160310.2/client/gethosts/0000755000015600001650000000000012670364532021571 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/client/gethosts/gethost_test.go0000644000015600001650000000565512670364255024651 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package gethosts import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/external/murmur3" ) func TestGetHosts(t *testing.T) { TestingT(t) } type getHostsSuite struct{} var _ = Suite(&getHostsSuite{}) func (s *getHostsSuite) TestNew(c *C) { gh := New("foobar", "http://where/hosts", 10*time.Second) c.Check(gh.hash, Equals, fmt.Sprintf("%x", murmur3.Sum64([]byte("foobar")))) c.Check(gh.endpointUrl, Equals, "http://where/hosts") } func (s *getHostsSuite) TestGet(c *C) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { x := r.FormValue("h") b, err := json.Marshal(map[string]interface{}{ "domain": "example.com", "hosts": []string{"http://" + x}, }) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") w.Write(b) })) defer ts.Close() gh := New("foobar", ts.URL, 1*time.Second) res, err := gh.Get() c.Assert(err, IsNil) c.Check(*res, DeepEquals, Host{Domain: "example.com", Hosts: []string{"http://bdd2ae7116c85a45"}}) } func (s *getHostsSuite) TestGetTimeout(c *C) { started := make(chan bool, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { started <- true time.Sleep(700 * time.Millisecond) })) defer func() { <-started ts.Close() }() gh := New("foobar", ts.URL, 500*time.Millisecond) _, err := gh.Get() c.Check(err, ErrorMatches, ".*closed.*") } func (s *getHostsSuite) TestGetErrorScenarios(c *C) { status := make(chan int, 1) body := make(chan string, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(<-status) fmt.Fprintf(w, "%s", <-body) })) defer ts.Close() gh := New("foobar", ts.URL, 1*time.Second) scenario := func(st int, bdy string, expectedErr error) { status <- st body <- bdy _, err := gh.Get() c.Check(err, Equals, expectedErr) } scenario(http.StatusBadRequest, "", ErrRequest) scenario(http.StatusInternalServerError, "", ErrInternal) scenario(http.StatusGatewayTimeout, "", ErrTemporary) scenario(http.StatusOK, "{", ErrTemporary) scenario(http.StatusOK, "{}", ErrTemporary) scenario(http.StatusOK, `{"domain": "example.com"}`, ErrTemporary) scenario(http.StatusOK, `{"hosts": ["one"]}`, nil) } ubuntu-push-0.68+16.04.20160310.2/client/gethosts/gethost.go0000644000015600001650000000472712670364255023611 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ // Package gethosts implements finding hosts to connect to for delivery of notifications. package gethosts import ( "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "time" "launchpad.net/ubuntu-push/external/murmur3" http13 "launchpad.net/ubuntu-push/http13client" ) // GetHost implements finding hosts to connect to consulting a remote endpoint providing a hash of the device identifier. type GetHost struct { hash string endpointUrl string cli *http13.Client } // New makes a GetHost. func New(deviceId, endpointUrl string, timeout time.Duration) *GetHost { hash := murmur3.Sum64([]byte(deviceId)) return &GetHost{ hash: fmt.Sprintf("%x", hash), endpointUrl: endpointUrl, cli: &http13.Client{ Transport: &http13.Transport{TLSHandshakeTimeout: timeout}, Timeout: timeout, }, } } // Host contains the domain and hosts returned by the remote endpoint type Host struct { Domain string Hosts []string } var ( ErrRequest = errors.New("request was not accepted") ErrInternal = errors.New("remote had an internal error") ErrTemporary = errors.New("remote had a temporary error") ) // Get gets a list of hosts consulting the endpoint. func (gh *GetHost) Get() (*Host, error) { resp, err := gh.cli.Get(gh.endpointUrl + "?h=" + gh.hash) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { switch { case resp.StatusCode == http.StatusInternalServerError: return nil, ErrInternal case resp.StatusCode > http.StatusInternalServerError: return nil, ErrTemporary default: return nil, ErrRequest } } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } var parsed Host err = json.Unmarshal(body, &parsed) if err != nil { return nil, ErrTemporary } if len(parsed.Hosts) == 0 { return nil, ErrTemporary } return &parsed, nil } ubuntu-push-0.68+16.04.20160310.2/client/client.go0000644000015600001650000004225012670364270021540 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ // Package client implements the Ubuntu Push Notifications client-side // daemon. package client import ( "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "encoding/pem" "errors" "fmt" "io/ioutil" "net/url" "os" "os/exec" "strings" "launchpad.net/ubuntu-push/accounts" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/bus/connectivity" "launchpad.net/ubuntu-push/bus/networkmanager" "launchpad.net/ubuntu-push/bus/systemimage" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/client/service" "launchpad.net/ubuntu-push/client/session" "launchpad.net/ubuntu-push/client/session/seenstate" "launchpad.net/ubuntu-push/config" "launchpad.net/ubuntu-push/identifier" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/poller" "launchpad.net/ubuntu-push/protocol" "launchpad.net/ubuntu-push/util" ) // ClientConfig holds the client configuration type ClientConfig struct { connectivity.ConnectivityConfig // q.v. // A reasonably large timeout for receive/answer pairs ExchangeTimeout config.ConfigTimeDuration `json:"exchange_timeout"` // A timeout to use when trying to connect to the server ConnectTimeout config.ConfigTimeDuration `json:"connect_timeout"` // The server to connect to or url to query for hosts to connect to Addr string // Host list management HostsCachingExpiryTime config.ConfigTimeDuration `json:"hosts_cache_expiry"` // potentially refresh host list after ExpectAllRepairedTime config.ConfigTimeDuration `json:"expect_all_repaired"` // worth retrying all servers after // The PEM-encoded server certificate CertPEMFile string `json:"cert_pem_file"` // How to invoke the auth helper AuthHelper string `json:"auth_helper"` SessionURL string `json:"session_url"` RegistrationURL string `json:"registration_url"` // The logging level (one of "debug", "info", "error") LogLevel logger.ConfigLogLevel `json:"log_level"` // fallback values for simplified notification usage FallbackVibration *launch_helper.Vibration `json:"fallback_vibration"` FallbackSound string `json:"fallback_sound"` // times for the poller PollInterval config.ConfigTimeDuration `json:"poll_interval"` PollSettle config.ConfigTimeDuration `json:"poll_settle"` PollNetworkWait config.ConfigTimeDuration `json:"poll_net_wait"` PollPolldWait config.ConfigTimeDuration `json:"poll_polld_wait"` PollDoneWait config.ConfigTimeDuration `json:"poll_done_wait"` PollBusyWait config.ConfigTimeDuration `json:"poll_busy_wait"` } // PushService is the interface we use of service.PushService. type PushService interface { // Start starts the service. Start() error // Unregister unregisters the token for appId. Unregister(appId string) error } type PostalService interface { // Starts the service Start() error // Post converts a push message into a presentable notification // and a postal message, presents the former and stores the // latter in the application's mailbox. Post(app *click.AppId, nid string, payload json.RawMessage) // IsRunning() returns whether the service is running IsRunning() bool // Stop() stops the service Stop() } // PushClient is the Ubuntu Push Notifications client-side daemon. type PushClient struct { leveldbPath string configPath string config ClientConfig log logger.Logger pem []byte idder identifier.Id deviceId string connectivityEndp bus.Endpoint systemImageEndp bus.Endpoint systemImageInfo *systemimage.InfoResult connCh chan bool session session.ClientSession sessionConnectedCh chan uint32 pushService PushService postalService PostalService unregisterCh chan *click.AppId trackAddressees map[string]*click.AppId installedChecker click.InstalledChecker poller poller.Poller accountsCh <-chan accounts.Changed // session-side channels broadcastCh chan *session.BroadcastNotification notificationsCh chan session.AddressedNotification } // Creates a new Ubuntu Push Notifications client-side daemon that will use // the given configuration file. func NewPushClient(configPath string, leveldbPath string) *PushClient { return &PushClient{ configPath: configPath, leveldbPath: leveldbPath, broadcastCh: make(chan *session.BroadcastNotification), notificationsCh: make(chan session.AddressedNotification), } } var newIdentifier = identifier.New // configure loads its configuration, and sets it up. func (client *PushClient) configure() error { _, err := os.Stat(client.configPath) if err != nil { return fmt.Errorf("config: %v", err) } err = config.ReadFiles(&client.config, client.configPath, "") if err != nil { return fmt.Errorf("config: %v", err) } // ignore spaces client.config.Addr = strings.Replace(client.config.Addr, " ", "", -1) if client.config.Addr == "" { return errors.New("no hosts specified") } // later, we'll be specifying more logging options in the config file client.log = logger.NewSimpleLogger(os.Stderr, client.config.LogLevel.Level()) clickUser, err := click.User() if err != nil { return fmt.Errorf("libclick: %v", err) } // overridden for testing client.installedChecker = clickUser client.unregisterCh = make(chan *click.AppId, 10) // overridden for testing client.idder, err = newIdentifier() if err != nil { return err } client.connectivityEndp = bus.SystemBus.Endpoint(networkmanager.BusAddress, client.log) client.systemImageEndp = bus.SystemBus.Endpoint(systemimage.BusAddress, client.log) client.connCh = make(chan bool, 1) client.sessionConnectedCh = make(chan uint32, 1) client.accountsCh = accounts.Watch() if client.config.CertPEMFile != "" { client.pem, err = ioutil.ReadFile(client.config.CertPEMFile) if err != nil { return fmt.Errorf("reading PEM file: %v", err) } // sanity check p, _ := pem.Decode(client.pem) if p == nil { return fmt.Errorf("no PEM found in PEM file") } } return nil } // deriveSessionConfig dervies the session configuration from the client configuration bits. func (client *PushClient) deriveSessionConfig(info map[string]interface{}) session.ClientSessionConfig { return session.ClientSessionConfig{ ConnectTimeout: client.config.ConnectTimeout.TimeDuration(), ExchangeTimeout: client.config.ExchangeTimeout.TimeDuration(), HostsCachingExpiryTime: client.config.HostsCachingExpiryTime.TimeDuration(), ExpectAllRepairedTime: client.config.ExpectAllRepairedTime.TimeDuration(), PEM: client.pem, Info: info, AuthGetter: client.getAuthorization, AuthURL: client.config.SessionURL, AddresseeChecker: client, BroadcastCh: client.broadcastCh, NotificationsCh: client.notificationsCh, } } // derivePushServiceSetup derives the service setup from the client configuration bits. func (client *PushClient) derivePushServiceSetup() (*service.PushServiceSetup, error) { setup := new(service.PushServiceSetup) purl, err := url.Parse(client.config.RegistrationURL) if err != nil { return nil, fmt.Errorf("cannot parse registration url: %v", err) } setup.RegURL = purl setup.DeviceId = client.deviceId setup.AuthGetter = client.getAuthorization setup.InstalledChecker = client.installedChecker return setup, nil } // derivePostalServiceSetup derives the service setup from the client configuration bits. func (client *PushClient) derivePostalServiceSetup() *service.PostalServiceSetup { return &service.PostalServiceSetup{ InstalledChecker: client.installedChecker, FallbackVibration: client.config.FallbackVibration, FallbackSound: client.config.FallbackSound, } } // derivePollerSetup derives the Poller setup from the client configuration bits. func (client *PushClient) derivePollerSetup() *poller.PollerSetup { return &poller.PollerSetup{ Times: poller.Times{ AlarmInterval: client.config.PollInterval.TimeDuration(), SessionStateSettle: client.config.PollSettle.TimeDuration(), NetworkWait: client.config.PollNetworkWait.TimeDuration(), PolldWait: client.config.PollPolldWait.TimeDuration(), DoneWait: client.config.PollDoneWait.TimeDuration(), BusyWait: client.config.PollBusyWait.TimeDuration(), }, Log: client.log, SessionStateGetter: client.session, } } // getAuthorization gets the authorization blob to send to the server func (client *PushClient) getAuthorization(url string) string { client.log.Debugf("getting authorization for %s", url) // using a helper, for now at least if len(client.config.AuthHelper) == 0 { // do nothing if helper is unset or empty return "" } auth, err := exec.Command(client.config.AuthHelper, url).Output() if err != nil { // For now we just log the error, as we don't want to block unauthorized users client.log.Errorf("unable to get the authorization token from the account: %v", err) return "" } else { return strings.TrimSpace(string(auth)) } } // getDeviceId gets the identifier for the device func (client *PushClient) getDeviceId() error { baseId := client.idder.String() b, err := hex.DecodeString(baseId) if err != nil { return fmt.Errorf("machine-id should be hex: %v", err) } h := sha256.Sum224(b) client.deviceId = base64.StdEncoding.EncodeToString(h[:]) return nil } // takeTheBus starts the connection(s) to D-Bus and sets up associated event channels func (client *PushClient) takeTheBus() error { cs := connectivity.New(client.connectivityEndp, client.config.ConnectivityConfig, client.log) go cs.Track(client.connCh) util.NewAutoRedialer(client.systemImageEndp).Redial() sysimg := systemimage.New(client.systemImageEndp, client.log) info, err := sysimg.Information() if err != nil { return err } client.systemImageInfo = info return err } // initSessionAndPoller creates the session and the poller objects func (client *PushClient) initSessionAndPoller() error { info := map[string]interface{}{ "device": client.systemImageInfo.Device, "channel": client.systemImageInfo.Channel, "build_number": client.systemImageInfo.BuildNumber, } sess, err := session.NewSession(client.config.Addr, client.deriveSessionConfig(info), client.deviceId, client.seenStateFactory, client.log) if err != nil { return err } client.session = sess sess.KeepConnection() client.poller = poller.New(client.derivePollerSetup()) return nil } // runPoller starts and runs the poller func (client *PushClient) runPoller() error { if err := client.poller.Start(); err != nil { return err } if err := client.poller.Run(); err != nil { return err } return nil } // seenStateFactory returns a SeenState for the session func (client *PushClient) seenStateFactory() (seenstate.SeenState, error) { if client.leveldbPath == "" { return seenstate.NewSeenState() } else { return seenstate.NewSqliteSeenState(client.leveldbPath) } } // StartAddresseeBatch starts a batch of checks for addressees. func (client *PushClient) StartAddresseeBatch() { client.trackAddressees = make(map[string]*click.AppId, 10) } // CheckForAddressee check for the addressee presence. func (client *PushClient) CheckForAddressee(notif *protocol.Notification) *click.AppId { appId := notif.AppId parsed, ok := client.trackAddressees[appId] if ok { return parsed } parsed, err := click.ParseAndVerifyAppId(appId, client.installedChecker) switch err { default: client.log.Debugf("notification %#v for invalid app id %#v.", notif.MsgId, notif.AppId) case click.ErrMissingApp: client.log.Debugf("notification %#v for missing app id %#v.", notif.MsgId, notif.AppId) client.unregisterCh <- parsed parsed = nil case nil: } client.trackAddressees[appId] = parsed return parsed } // handleUnregister deals with tokens of uninstalled apps func (client *PushClient) handleUnregister(app *click.AppId) { if !client.installedChecker.Installed(app, false) { // xxx small chance of race here, in case the app gets // reinstalled and registers itself before we finish // the unregister; we need click and app launching // collaboration to do better. we redo the hasPackage // check here just before to keep the race window as // small as possible err := client.pushService.Unregister(app.Original()) // XXX WIP if err != nil { client.log.Errorf("unregistering %s: %s", app, err) } else { client.log.Debugf("unregistered token for %s", app) } } } // filterBroadcastNotification finds out if the notification is about an actual // upgrade for the device. It expects msg.Decoded entries to look // like: // // { // "IMAGE-CHANNEL/DEVICE-MODEL": [BUILD-NUMBER, CHANNEL-ALIAS] // ... // } func (client *PushClient) filterBroadcastNotification(msg *session.BroadcastNotification) bool { n := len(msg.Decoded) if n == 0 { return false } // they are all for us, consider last last := msg.Decoded[n-1] tag := fmt.Sprintf("%s/%s", client.systemImageInfo.Channel, client.systemImageInfo.Device) entry, ok := last[tag] if !ok { return false } pair, ok := entry.([]interface{}) if !ok { return false } if len(pair) < 1 { return false } _, ok = pair[0].(float64) // ok means it sanity checks, let the helper check for build number etc return ok } // handleBroadcastNotification deals with receiving a broadcast notification func (client *PushClient) handleBroadcastNotification(msg *session.BroadcastNotification) error { if !client.filterBroadcastNotification(msg) { client.log.Debugf("not posting broadcast notification %d; filtered.", msg.TopLevel) return nil } // marshal the last decoded msg to json payload, err := json.Marshal(msg.Decoded[len(msg.Decoded)-1]) if err != nil { client.log.Errorf("while posting broadcast notification %d: %v", msg.TopLevel, err) return err } appId, _ := click.ParseAppId("_ubuntu-system-settings") client.postalService.Post(appId, "", payload) client.log.Debugf("posted broadcast notification %d.", msg.TopLevel) return nil } // handleUnicastNotification deals with receiving a unicast notification func (client *PushClient) handleUnicastNotification(anotif session.AddressedNotification) error { app := anotif.To msg := anotif.Notification client.postalService.Post(app, msg.MsgId, msg.Payload) client.log.Debugf("posted unicast notification %s for %s.", msg.MsgId, msg.AppId) return nil } func (client *PushClient) handeConnNotification(conn bool) { client.session.HasConnectivity(conn) client.poller.HasConnectivity(conn) } // doLoop connects events with their handlers func (client *PushClient) doLoop(connhandler func(bool), bcasthandler func(*session.BroadcastNotification) error, ucasthandler func(session.AddressedNotification) error, unregisterhandler func(*click.AppId), accountshandler func()) { for { select { case <-client.accountsCh: accountshandler() case state := <-client.connCh: connhandler(state) case bcast := <-client.broadcastCh: bcasthandler(bcast) case aucast := <-client.notificationsCh: ucasthandler(aucast) case count := <-client.sessionConnectedCh: client.log.Debugf("session connected after %d attempts", count) case app := <-client.unregisterCh: unregisterhandler(app) } } } // doStart calls each of its arguments in order, returning the first non-nil // error (or nil at the end) func (client *PushClient) doStart(fs ...func() error) error { for _, f := range fs { if err := f(); err != nil { return err } } return nil } // Loop calls doLoop with the "real" handlers func (client *PushClient) Loop() { client.doLoop(client.handeConnNotification, client.handleBroadcastNotification, client.handleUnicastNotification, client.handleUnregister, client.session.ResetCookie, ) } func (client *PushClient) setupPushService() error { setup, err := client.derivePushServiceSetup() if err != nil { return err } client.pushService = service.NewPushService(setup, client.log) return nil } func (client *PushClient) startPushService() error { if err := client.pushService.Start(); err != nil { return err } return nil } func (client *PushClient) setupPostalService() error { setup := client.derivePostalServiceSetup() client.postalService = service.NewPostalService(setup, client.log) return nil } func (client *PushClient) startPostalService() error { if err := client.postalService.Start(); err != nil { return err } return nil } // Start calls doStart with the "real" starters func (client *PushClient) Start() error { return client.doStart( client.configure, client.getDeviceId, client.setupPushService, client.setupPostalService, client.startPushService, client.startPostalService, client.takeTheBus, client.initSessionAndPoller, client.runPoller, ) } ubuntu-push-0.68+16.04.20160310.2/client/client.dot0000644000015600001650000000505112670364255021722 0ustar pbuserpbgroup00000000000000digraph g { // both "neato" and "dot" produce reasonable & interesting outputs graph [rankdir=LR size="20,15" overlap="scale" splines="true"] node [fontname="Ubuntu Mono" fontsize=12 margin=0] edge [fontname="Ubuntu Mono" fontsize=10 decorate=true] // states node [] "INIT" [shape=doublecircle] "Error" [shape=doublecircle color="#990000"] "Configured" "Identified" "SysBusOK" [label="System\nBus\nGO"] "Waiting4Conn" [label="Waiting\nfor\nConnectivity"] "Connected" [label="Network\nConnected"] "SesBusOK" [label="Session\nBus\nGO"] "Running" "Notified" "Shown" [label="Notification\nShown"] "Clicked" // auto-transitions node [shape=triangle] "read config" [label="read\nconfig"] "get system id" [label="get\nsystem\nid"] "conn sys bus" [label="connect\nsystem\nbus"] "watch conn" [label="watch\nconnectivity"] "conn ses bus" [label="connect\nsession\nbus"] "start session" [label="start\npush\nsession"] "show notification" [label="show\nnotification"] "dispatch URL" [label="dispatch\nURL"] // "INIT" -> "read config" "Configured" -> "get system id" "Identified" -> { "conn sys bus" "conn ses bus" } "SysBusOK" -> "watch conn" "Connected" -> "start session" "Notified" -> "show notification" "Clicked" -> "dispatch URL" -> "SesBusOK" "Shown" -> "SesBusOK" // XXX state:state auto-transition? // events edge [color="#000099"] "Waiting4Conn" -> "Connected" [label="connected"] "Waiting4Conn" -> "Waiting4Conn" [label="disconnected"] "SesBusOK" -> "Notified" [label="notification\narrived"] "SesBusOK" -> "Clicked" [label="user\nclicked\nnotification"] { "Connected" "Running" } -> "Waiting4Conn" [constraint=false label="disconnected"] "Running" -> "SesBusOK" [constraint=false style=dotted label=notification] "Shown" -> "Running" [constraint=false style=dotted label=shown] // OKs edge [color="#009900" label="OK"] "read config" -> "Configured" "get system id" -> "Identified" "conn sys bus" -> "SysBusOK" "conn ses bus" -> "SesBusOK" "watch conn" -> "Waiting4Conn" "start session" -> "Running" "show notification" -> "Shown" //err edge [color="#990000" label="err" constraint=false] { "read config" "get system id" } -> Error "conn ses bus" -> "conn ses bus" "conn sys bus" -> "conn sys bus" "watch conn" -> "conn sys bus" "start session" -> "start session" } ubuntu-push-0.68+16.04.20160310.2/client/session/0000755000015600001650000000000012670364532021414 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/client/session/seenstate/0000755000015600001650000000000012670364532023407 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/client/session/seenstate/seenstate.go0000644000015600001650000000421212670364255025732 0ustar pbuserpbgroup00000000000000/* 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 seenstate holds implementations of the SeenState that the client // session uses to keep track of what messages it has seen. package seenstate import ( "launchpad.net/ubuntu-push/protocol" ) type SeenState interface { // Set() (re)sets the given level to the given value. SetLevel(level string, top int64) error // GetAll() returns a "simple" map of the current levels. GetAllLevels() (map[string]int64, error) // FilterBySeen filters notifications already seen, keep track // of them as well. FilterBySeen([]protocol.Notification) ([]protocol.Notification, error) // Close closes state. Close() } type memSeenState struct { levels map[string]int64 seenMsgs map[string]bool } func (m *memSeenState) SetLevel(level string, top int64) error { m.levels[level] = top return nil } func (m *memSeenState) GetAllLevels() (map[string]int64, error) { return m.levels, nil } func (m *memSeenState) FilterBySeen(notifs []protocol.Notification) ([]protocol.Notification, error) { acc := make([]protocol.Notification, 0, len(notifs)) for _, notif := range notifs { seen := m.seenMsgs[notif.MsgId] if seen { continue } m.seenMsgs[notif.MsgId] = true acc = append(acc, notif) } return acc, nil } func (m *memSeenState) Close() { } var _ SeenState = (*memSeenState)(nil) // NewSeenState returns an implementation of SeenState that is memory-based and // does not save state. func NewSeenState() (SeenState, error) { return &memSeenState{ levels: make(map[string]int64), seenMsgs: make(map[string]bool), }, nil } ubuntu-push-0.68+16.04.20160310.2/client/session/seenstate/seenstate_test.go0000644000015600001650000000461712670364255027002 0ustar pbuserpbgroup00000000000000/* 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 seenstate import ( "testing" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/protocol" ) func TestSeenState(t *testing.T) { TestingT(t) } type ssSuite struct { constructor func() (SeenState, error) } var _ = Suite(&ssSuite{}) func (s *ssSuite) SetUpSuite(c *C) { s.constructor = NewSeenState } func (s *ssSuite) TestAllTheLevelThings(c *C) { var err error var ss SeenState // checks NewSeenState returns a SeenState ss, err = s.constructor() // and that it works c.Assert(err, IsNil) // setting a couple of things, sets them c.Check(ss.SetLevel("this", 12), IsNil) c.Check(ss.SetLevel("that", 42), IsNil) all, err := ss.GetAllLevels() c.Check(err, IsNil) c.Check(all, DeepEquals, map[string]int64{"this": 12, "that": 42}) // re-setting one of them, resets it c.Check(ss.SetLevel("this", 999), IsNil) all, err = ss.GetAllLevels() c.Check(err, IsNil) c.Check(all, DeepEquals, map[string]int64{"this": 999, "that": 42}) // huzzah } func (s *ssSuite) TestFilterBySeen(c *C) { var err error var ss SeenState ss, err = s.constructor() // and that it works c.Assert(err, IsNil) n1 := protocol.Notification{MsgId: "m1"} n2 := protocol.Notification{MsgId: "m2"} n3 := protocol.Notification{MsgId: "m3"} n4 := protocol.Notification{MsgId: "m4"} n5 := protocol.Notification{MsgId: "m5"} res, err := ss.FilterBySeen([]protocol.Notification{n1, n2, n3}) c.Assert(err, IsNil) // everything wasn't seen yet c.Check(res, DeepEquals, []protocol.Notification{n1, n2, n3}) res, err = ss.FilterBySeen([]protocol.Notification{n1, n3, n4, n5}) c.Assert(err, IsNil) // already seen n1-n3 removed c.Check(res, DeepEquals, []protocol.Notification{n4, n5}) // corner case res, err = ss.FilterBySeen([]protocol.Notification{}) c.Assert(err, IsNil) c.Assert(res, HasLen, 0) } ubuntu-push-0.68+16.04.20160310.2/client/session/seenstate/sqlseenstate.go0000644000015600001650000000631012670364255026453 0ustar pbuserpbgroup00000000000000/* 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 seenstate import ( "database/sql" "fmt" "strings" _ "code.google.com/p/gosqlite/sqlite3" "launchpad.net/ubuntu-push/protocol" ) type sqliteSeenState struct { db *sql.DB } // NewSqliteSeenState returns an implementation of SeenState that // keeps and persists the state in an sqlite database. func NewSqliteSeenState(filename string) (SeenState, error) { db, err := sql.Open("sqlite3", filename) if err != nil { return nil, fmt.Errorf("cannot open sqlite level map %#v: %v", filename, err) } _, err = db.Exec("CREATE TABLE IF NOT EXISTS level_map (level text primary key, top integer)") if err != nil { return nil, fmt.Errorf("cannot (re)create sqlite level map table: %v", err) } _, err = db.Exec("CREATE TABLE IF NOT EXISTS seen_msgs (id text primary key)") if err != nil { return nil, fmt.Errorf("cannot (re)create sqlite seen msgs table: %v", err) } return &sqliteSeenState{db}, nil } // Closes closes the underlying db. func (ps *sqliteSeenState) Close() { ps.db.Close() } func (ps *sqliteSeenState) SetLevel(level string, top int64) error { _, err := ps.db.Exec("REPLACE INTO level_map (level, top) VALUES (?, ?)", level, top) if err != nil { return fmt.Errorf("cannot set %#v to %#v in level map: %v", level, top, err) } return nil } func (ps *sqliteSeenState) GetAllLevels() (map[string]int64, error) { rows, err := ps.db.Query("SELECT * FROM level_map") if err != nil { return nil, fmt.Errorf("cannot retrieve levels from sqlite level map: %v", err) } m := map[string]int64{} for rows.Next() { var level string var top int64 err = rows.Scan(&level, &top) if err != nil { return nil, fmt.Errorf("cannot read level from sqlite level map: %v", err) } m[level] = top } return m, nil } func (ps *sqliteSeenState) dropPrevThan(msgId string) error { _, err := ps.db.Exec("DELETE FROM seen_msgs WHERE rowid < (SELECT rowid FROM seen_msgs WHERE id = ?)", msgId) return err } func (ps *sqliteSeenState) FilterBySeen(notifs []protocol.Notification) ([]protocol.Notification, error) { if len(notifs) == 0 { return nil, nil } acc := make([]protocol.Notification, 0, len(notifs)) for _, notif := range notifs { _, err := ps.db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", notif.MsgId) if err != nil { if strings.HasSuffix(err.Error(), "UNIQUE constraint failed: seen_msgs.id") { continue } return nil, fmt.Errorf("cannot insert %#v in seen msgs: %v", notif.MsgId, err) } acc = append(acc, notif) } err := ps.dropPrevThan(notifs[0].MsgId) if err != nil { return nil, fmt.Errorf("cannot delete obsolete seen msgs: %v", err) } return acc, nil } ubuntu-push-0.68+16.04.20160310.2/client/session/seenstate/sqlseenstate_test.go0000644000015600001650000001152012670364255027511 0ustar pbuserpbgroup00000000000000/* 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 seenstate import ( "database/sql" _ "code.google.com/p/gosqlite/sqlite3" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/protocol" ) type sqlsSuite struct{ ssSuite } var _ = Suite(&sqlsSuite{}) func (s *sqlsSuite) SetUpSuite(c *C) { s.constructor = func() (SeenState, error) { return NewSqliteSeenState(":memory:") } } func (s *sqlsSuite) TestNewCanFail(c *C) { sqls, err := NewSqliteSeenState("/does/not/exist") c.Assert(sqls, IsNil) c.Check(err, NotNil) } func (s *sqlsSuite) TestSetCanFail(c *C) { dir := c.MkDir() filename := dir + "test.db" db, err := sql.Open("sqlite3", filename) c.Assert(err, IsNil) // create the wrong kind of table _, err = db.Exec("CREATE TABLE level_map (foo)") c.Assert(err, IsNil) // sqls, err := NewSqliteSeenState(filename) c.Check(err, IsNil) c.Assert(sqls, NotNil) err = sqls.SetLevel("foo", 42) c.Check(err, ErrorMatches, "cannot set .*") } func (s *sqlsSuite) TestGetAllCanFail(c *C) { dir := c.MkDir() filename := dir + "test.db" db, err := sql.Open("sqlite3", filename) c.Assert(err, IsNil) // create the wrong kind of table _, err = db.Exec("CREATE TABLE level_map AS SELECT 'what'") c.Assert(err, IsNil) // sqls, err := NewSqliteSeenState(filename) c.Check(err, IsNil) c.Assert(sqls, NotNil) all, err := sqls.GetAllLevels() c.Check(all, IsNil) c.Check(err, ErrorMatches, "cannot read level .*") } func (s *sqlsSuite) TestGetAllCanFailDifferently(c *C) { dir := c.MkDir() filename := dir + "test.db" db, err := sql.Open("sqlite3", filename) c.Assert(err, IsNil) // create a view with the name the table will have _, err = db.Exec("CREATE TABLE foo (foo)") c.Assert(err, IsNil) _, err = db.Exec("CREATE VIEW level_map AS SELECT * FROM foo") c.Assert(err, IsNil) // break the view _, err = db.Exec("DROP TABLE foo") c.Assert(err, IsNil) // sqls, err := NewSqliteSeenState(filename) c.Check(err, IsNil) c.Assert(sqls, NotNil) all, err := sqls.GetAllLevels() c.Check(all, IsNil) c.Check(err, ErrorMatches, "cannot retrieve levels .*") } func (s *sqlsSuite) TestFilterBySeenCanFail(c *C) { dir := c.MkDir() filename := dir + "test.db" db, err := sql.Open("sqlite3", filename) c.Assert(err, IsNil) // create the wrong kind of table _, err = db.Exec("CREATE TABLE seen_msgs AS SELECT 'what'") c.Assert(err, IsNil) // sqls, err := NewSqliteSeenState(filename) c.Check(err, IsNil) c.Assert(sqls, NotNil) n1 := protocol.Notification{MsgId: "m1"} res, err := sqls.FilterBySeen([]protocol.Notification{n1}) c.Check(res, IsNil) c.Check(err, ErrorMatches, "cannot insert .*") } func (s *sqlsSuite) TestClose(c *C) { dir := c.MkDir() filename := dir + "test.db" sqls, err := NewSqliteSeenState(filename) c.Check(err, IsNil) c.Assert(sqls, NotNil) sqls.Close() } func (s *sqlsSuite) TestDropPrevThan(c *C) { dir := c.MkDir() filename := dir + "test.db" db, err := sql.Open("sqlite3", filename) c.Assert(err, IsNil) sqls, err := NewSqliteSeenState(filename) c.Check(err, IsNil) c.Assert(sqls, NotNil) _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m1") c.Assert(err, IsNil) _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m2") c.Assert(err, IsNil) _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m3") c.Assert(err, IsNil) _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m4") c.Assert(err, IsNil) _, err = db.Exec("INSERT INTO seen_msgs (id) VALUES (?)", "m5") c.Assert(err, IsNil) rows, err := db.Query("SELECT COUNT(*) FROM seen_msgs") c.Assert(err, IsNil) rows.Next() var i int err = rows.Scan(&i) c.Assert(err, IsNil) c.Check(i, Equals, 5) rows.Close() err = sqls.(*sqliteSeenState).dropPrevThan("m3") c.Assert(err, IsNil) rows, err = db.Query("SELECT COUNT(*) FROM seen_msgs") c.Assert(err, IsNil) rows.Next() err = rows.Scan(&i) c.Assert(err, IsNil) c.Check(i, Equals, 3) rows.Close() var msgId string rows, err = db.Query("SELECT * FROM seen_msgs") rows.Next() err = rows.Scan(&msgId) c.Assert(err, IsNil) c.Check(msgId, Equals, "m3") rows.Next() err = rows.Scan(&msgId) c.Assert(err, IsNil) c.Check(msgId, Equals, "m4") rows.Next() err = rows.Scan(&msgId) c.Assert(err, IsNil) c.Check(msgId, Equals, "m5") rows.Close() } ubuntu-push-0.68+16.04.20160310.2/client/session/session_test.go0000644000015600001650000016354712670364255024507 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package session import ( "crypto/tls" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "net/http/httptest" "reflect" "testing" "time" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/client/gethosts" "launchpad.net/ubuntu-push/client/session/seenstate" "launchpad.net/ubuntu-push/protocol" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" "launchpad.net/ubuntu-push/util" ) func TestSession(t *testing.T) { TestingT(t) } // // helpers! candidates to live in their own ../testing/ package. // type xAddr string func (x xAddr) Network() string { return "<:>" } func (x xAddr) String() string { return string(x) } // testConn (roughly based on the one in protocol_test) type testConn struct { Name string Deadlines []time.Duration Writes [][]byte WriteCondition condition.Interface DeadlineCondition condition.Interface CloseCondition condition.Interface } func (tc *testConn) LocalAddr() net.Addr { return xAddr(tc.Name) } func (tc *testConn) RemoteAddr() net.Addr { return xAddr(tc.Name) } func (tc *testConn) Close() error { if tc.CloseCondition == nil || tc.CloseCondition.OK() { return nil } else { return errors.New("closer on fire") } } func (tc *testConn) SetDeadline(t time.Time) error { tc.Deadlines = append(tc.Deadlines, t.Sub(time.Now())) if tc.DeadlineCondition == nil || tc.DeadlineCondition.OK() { return nil } else { return errors.New("deadliner on fire") } } func (tc *testConn) SetReadDeadline(t time.Time) error { panic("SetReadDeadline not implemented.") } func (tc *testConn) SetWriteDeadline(t time.Time) error { panic("SetWriteDeadline not implemented.") } func (tc *testConn) Read(buf []byte) (n int, err error) { panic("Read not implemented.") } func (tc *testConn) Write(buf []byte) (int, error) { store := make([]byte, len(buf)) copy(store, buf) tc.Writes = append(tc.Writes, store) if tc.WriteCondition == nil || tc.WriteCondition.OK() { return len(store), nil } else { return -1, errors.New("writer on fire") } } // test protocol (from session_test) type testProtocol struct { up chan interface{} down chan interface{} } // takeNext takes a value from given channel with a 5s timeout func takeNext(ch <-chan interface{}) interface{} { select { case <-time.After(5 * time.Second): panic("test protocol exchange stuck: too long waiting") case v := <-ch: return v } return nil } func (c *testProtocol) SetDeadline(t time.Time) { deadAfter := t.Sub(time.Now()) deadAfter = (deadAfter + time.Millisecond/2) / time.Millisecond * time.Millisecond c.down <- fmt.Sprintf("deadline %v", deadAfter) } func (c *testProtocol) ReadMessage(dest interface{}) error { switch v := takeNext(c.up).(type) { case error: return v default: // make sure JSON.Unmarshal works with dest var marshalledMsg []byte marshalledMsg, err := json.Marshal(v) if err != nil { return fmt.Errorf("can't jsonify test value %v: %s", v, err) } return json.Unmarshal(marshalledMsg, dest) } return nil } func (c *testProtocol) WriteMessage(src interface{}) error { // make sure JSON.Marshal works with src _, err := json.Marshal(src) if err != nil { return err } val := reflect.ValueOf(src) if val.Kind() == reflect.Ptr { src = val.Elem().Interface() } c.down <- src switch v := takeNext(c.up).(type) { case error: return v } return nil } // brokenSeenState is a SeenState that always breaks type brokenSeenState struct{} func (*brokenSeenState) SetLevel(string, int64) error { return errors.New("broken.") } func (*brokenSeenState) GetAllLevels() (map[string]int64, error) { return nil, errors.New("broken.") } func (*brokenSeenState) Close() {} func (*brokenSeenState) FilterBySeen([]protocol.Notification) ([]protocol.Notification, error) { return nil, errors.New("broken.") } ///// type clientSessionSuite struct { log *helpers.TestLogger lvls func() (seenstate.SeenState, error) } func (cs *clientSessionSuite) SetUpTest(c *C) { cs.log = helpers.NewTestLogger(c, "debug") } // in-memory level map testing var _ = Suite(&clientSessionSuite{lvls: seenstate.NewSeenState}) // sqlite level map testing type clientSqlevelsSessionSuite struct{ clientSessionSuite } var _ = Suite(&clientSqlevelsSessionSuite{}) func (cs *clientSqlevelsSessionSuite) SetUpSuite(c *C) { cs.lvls = func() (seenstate.SeenState, error) { return seenstate.NewSqliteSeenState(":memory:") } } func (cs *clientSessionSuite) TestStateString(c *C) { for _, i := range []struct { v ClientSessionState s string }{ {Error, "Error"}, {Pristine, "Pristine"}, {Disconnected, "Disconnected"}, {Connected, "Connected"}, {Started, "Started"}, {Running, "Running"}, {Shutdown, "Shutdown"}, {Unknown, fmt.Sprintf("??? (%d)", Unknown)}, } { c.Check(i.v.String(), Equals, i.s) } } /**************************************************************** parseServerAddrSpec() tests ****************************************************************/ func (cs *clientSessionSuite) TestParseServerAddrSpec(c *C) { hEp, fallbackHosts := parseServerAddrSpec("http://foo/hosts") c.Check(hEp, Equals, "http://foo/hosts") c.Check(fallbackHosts, IsNil) hEp, fallbackHosts = parseServerAddrSpec("foo:443") c.Check(hEp, Equals, "") c.Check(fallbackHosts, DeepEquals, []string{"foo:443"}) hEp, fallbackHosts = parseServerAddrSpec("foo:443|bar:443") c.Check(hEp, Equals, "") c.Check(fallbackHosts, DeepEquals, []string{"foo:443", "bar:443"}) } /**************************************************************** NewSession() tests ****************************************************************/ func dummyConf() ClientSessionConfig { return ClientSessionConfig{ BroadcastCh: make(chan *BroadcastNotification, 5), NotificationsCh: make(chan AddressedNotification, 5), } } func (cs *clientSessionSuite) TestNewSessionPlainWorks(c *C) { sess, err := NewSession("foo:443", dummyConf(), "", cs.lvls, cs.log) c.Check(sess, NotNil) c.Check(err, IsNil) c.Check(sess.fallbackHosts, DeepEquals, []string{"foo:443"}) // the session is happy and redial delayer is default c.Check(sess.ShouldDelay(), Equals, false) c.Check(fmt.Sprintf("%#v", sess.redialDelay), Equals, fmt.Sprintf("%#v", redialDelay)) c.Check(sess.redialDelays, DeepEquals, util.Timeouts()) // but no root CAs set c.Check(sess.TLS.RootCAs, IsNil) c.Check(sess.State(), Equals, Pristine) c.Check(sess.stopCh, NotNil) c.Check(sess.cmdCh, NotNil) } func (cs *clientSessionSuite) TestNewSessionHostEndpointWorks(c *C) { sess, err := NewSession("http://foo/hosts", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) c.Check(sess.getHost, NotNil) } func (cs *clientSessionSuite) TestNewSessionPEMWorks(c *C) { pem, err := ioutil.ReadFile(helpers.SourceRelative("../../server/acceptance/ssl/testing.cert")) c.Assert(err, IsNil) conf := ClientSessionConfig{PEM: pem} sess, err := NewSession("", conf, "wah", cs.lvls, cs.log) c.Check(sess, NotNil) c.Assert(err, IsNil) c.Check(sess.TLS.RootCAs, NotNil) } func (cs *clientSessionSuite) TestNewSessionBadPEMFileContentFails(c *C) { badpem := []byte("This is not the PEM you're looking for.") conf := ClientSessionConfig{PEM: badpem} sess, err := NewSession("", conf, "wah", cs.lvls, cs.log) c.Check(sess, IsNil) c.Check(err, NotNil) } func (cs *clientSessionSuite) TestNewSessionBadSeenStateFails(c *C) { ferr := func() (seenstate.SeenState, error) { return nil, errors.New("Busted.") } sess, err := NewSession("", dummyConf(), "wah", ferr, cs.log) c.Check(sess, IsNil) c.Assert(err, NotNil) } /**************************************************************** getHosts() tests ****************************************************************/ func (cs *clientSessionSuite) TestGetHostsFallback(c *C) { fallback := []string{"foo:443", "bar:443"} sess := &clientSession{fallbackHosts: fallback} err := sess.getHosts() c.Assert(err, IsNil) c.Check(sess.deliveryHosts, DeepEquals, fallback) } type testHostGetter struct { domain string hosts []string err error } func (thg *testHostGetter) Get() (*gethosts.Host, error) { return &gethosts.Host{thg.domain, thg.hosts}, thg.err } func (cs *clientSessionSuite) TestGetHostsRemote(c *C) { hostGetter := &testHostGetter{"example.com", []string{"foo:443", "bar:443"}, nil} sess := &clientSession{getHost: hostGetter, timeSince: time.Since} err := sess.getHosts() c.Assert(err, IsNil) c.Check(sess.deliveryHosts, DeepEquals, []string{"foo:443", "bar:443"}) } func (cs *clientSessionSuite) TestGetHostsRemoteError(c *C) { sess, err := NewSession("", dummyConf(), "", cs.lvls, cs.log) c.Assert(err, IsNil) hostsErr := errors.New("failed") hostGetter := &testHostGetter{"", nil, hostsErr} sess.getHost = hostGetter err = sess.getHosts() c.Assert(err, Equals, hostsErr) c.Check(sess.deliveryHosts, IsNil) c.Check(sess.State(), Equals, Error) } func (cs *clientSessionSuite) TestGetHostsRemoteCaching(c *C) { hostGetter := &testHostGetter{"example.com", []string{"foo:443", "bar:443"}, nil} sess := &clientSession{ getHost: hostGetter, ClientSessionConfig: ClientSessionConfig{ HostsCachingExpiryTime: 2 * time.Hour, }, timeSince: time.Since, } err := sess.getHosts() c.Assert(err, IsNil) hostGetter.hosts = []string{"baz:443"} // cached err = sess.getHosts() c.Assert(err, IsNil) c.Check(sess.deliveryHosts, DeepEquals, []string{"foo:443", "bar:443"}) // expired sess.timeSince = func(ts time.Time) time.Duration { return 3 * time.Hour } err = sess.getHosts() c.Assert(err, IsNil) c.Check(sess.deliveryHosts, DeepEquals, []string{"baz:443"}) } func (cs *clientSessionSuite) TestGetHostsRemoteCachingReset(c *C) { hostGetter := &testHostGetter{"example.com", []string{"foo:443", "bar:443"}, nil} sess := &clientSession{ getHost: hostGetter, ClientSessionConfig: ClientSessionConfig{ HostsCachingExpiryTime: 2 * time.Hour, }, timeSince: time.Since, } err := sess.getHosts() c.Assert(err, IsNil) hostGetter.hosts = []string{"baz:443"} // cached err = sess.getHosts() c.Assert(err, IsNil) c.Check(sess.deliveryHosts, DeepEquals, []string{"foo:443", "bar:443"}) // reset sess.resetHosts() err = sess.getHosts() c.Assert(err, IsNil) c.Check(sess.deliveryHosts, DeepEquals, []string{"baz:443"}) } /**************************************************************** addAuthorization() tests ****************************************************************/ func (cs *clientSessionSuite) TestAddAuthorizationAddsAuthorization(c *C) { url := "xyzzy://" sess := &clientSession{Log: cs.log} sess.AuthGetter = func(url string) string { return url + " auth'ed" } sess.AuthURL = url c.Assert(sess.auth, Equals, "") err := sess.addAuthorization() c.Assert(err, IsNil) c.Check(sess.auth, Equals, "xyzzy:// auth'ed") } func (cs *clientSessionSuite) TestAddAuthorizationSkipsIfUnset(c *C) { sess := &clientSession{Log: cs.log} sess.AuthGetter = nil c.Assert(sess.auth, Equals, "") err := sess.addAuthorization() c.Assert(err, IsNil) c.Check(sess.auth, Equals, "") } /**************************************************************** startConnectionAttempt()/nextHostToTry()/started tests ****************************************************************/ func (cs *clientSessionSuite) TestStartConnectionAttempt(c *C) { since := time.Since(time.Time{}) sess := &clientSession{ ClientSessionConfig: ClientSessionConfig{ ExpectAllRepairedTime: 10 * time.Second, }, timeSince: func(ts time.Time) time.Duration { return since }, deliveryHosts: []string{"foo:443", "bar:443"}, } // start from first host sess.startConnectionAttempt() c.Check(sess.lastAttemptTimestamp, Not(Equals), 0) c.Check(sess.tryHost, Equals, 0) c.Check(sess.leftToTry, Equals, 2) since = 1 * time.Second sess.tryHost = 1 // just continue sess.startConnectionAttempt() c.Check(sess.tryHost, Equals, 1) sess.tryHost = 2 } func (cs *clientSessionSuite) TestStartConnectionAttemptNoHostsPanic(c *C) { since := time.Since(time.Time{}) sess := &clientSession{ ClientSessionConfig: ClientSessionConfig{ ExpectAllRepairedTime: 10 * time.Second, }, timeSince: func(ts time.Time) time.Duration { return since }, } c.Check(sess.startConnectionAttempt, PanicMatches, "should have got hosts from config or remote at this point") } func (cs *clientSessionSuite) TestNextHostToTry(c *C) { sess := &clientSession{ deliveryHosts: []string{"foo:443", "bar:443", "baz:443"}, tryHost: 0, leftToTry: 3, } c.Check(sess.nextHostToTry(), Equals, "foo:443") c.Check(sess.nextHostToTry(), Equals, "bar:443") c.Check(sess.nextHostToTry(), Equals, "baz:443") c.Check(sess.nextHostToTry(), Equals, "") c.Check(sess.nextHostToTry(), Equals, "") c.Check(sess.tryHost, Equals, 0) sess.leftToTry = 3 sess.tryHost = 1 c.Check(sess.nextHostToTry(), Equals, "bar:443") c.Check(sess.nextHostToTry(), Equals, "baz:443") c.Check(sess.nextHostToTry(), Equals, "foo:443") c.Check(sess.nextHostToTry(), Equals, "") c.Check(sess.nextHostToTry(), Equals, "") c.Check(sess.tryHost, Equals, 1) } func (cs *clientSessionSuite) TestStarted(c *C) { sess, err := NewSession("", dummyConf(), "", cs.lvls, cs.log) c.Assert(err, IsNil) sess.deliveryHosts = []string{"foo:443", "bar:443", "baz:443"} sess.tryHost = 1 sess.started() c.Check(sess.tryHost, Equals, 0) c.Check(sess.State(), Equals, Started) sess.started() c.Check(sess.tryHost, Equals, 2) } /**************************************************************** connect() tests ****************************************************************/ func (cs *clientSessionSuite) TestConnectFailsWithNoAddress(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.deliveryHosts = []string{"nowhere"} sess.clearShouldDelay() err = sess.connect() c.Check(sess.ShouldDelay(), Equals, true) c.Check(err, ErrorMatches, ".*connect.*address.*") c.Check(sess.State(), Equals, Error) } func (cs *clientSessionSuite) TestConnectConnects(c *C) { srv, err := net.Listen("tcp", "localhost:0") c.Assert(err, IsNil) defer srv.Close() sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.deliveryHosts = []string{srv.Addr().String()} sess.clearShouldDelay() err = sess.connect() c.Check(sess.ShouldDelay(), Equals, true) c.Check(err, IsNil) c.Check(sess.Connection, NotNil) c.Check(sess.State(), Equals, Connected) } func (cs *clientSessionSuite) TestConnectSecondConnects(c *C) { srv, err := net.Listen("tcp", "localhost:0") c.Assert(err, IsNil) defer srv.Close() sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.deliveryHosts = []string{"nowhere", srv.Addr().String()} sess.clearShouldDelay() err = sess.connect() c.Check(sess.ShouldDelay(), Equals, true) c.Check(err, IsNil) c.Check(sess.Connection, NotNil) c.Check(sess.State(), Equals, Connected) c.Check(sess.tryHost, Equals, 0) } func (cs *clientSessionSuite) TestConnectConnectFail(c *C) { srv, err := net.Listen("tcp", "localhost:0") c.Assert(err, IsNil) sess, err := NewSession(srv.Addr().String(), dummyConf(), "wah", cs.lvls, cs.log) srv.Close() c.Assert(err, IsNil) sess.deliveryHosts = []string{srv.Addr().String()} sess.clearShouldDelay() err = sess.connect() c.Check(sess.ShouldDelay(), Equals, true) c.Check(err, ErrorMatches, ".*connection refused") c.Check(sess.State(), Equals, Error) } type dumbRetrier struct{ stopped bool } func (*dumbRetrier) Redial() uint32 { return 0 } func (d *dumbRetrier) Stop() { d.stopped = true } // /**************************************************************** // AutoRedial() tests // ****************************************************************/ func (cs *clientSessionSuite) TestAutoRedialWorks(c *C) { // checks that AutoRedial sets up a retrier and tries redialing it sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) ar := new(dumbRetrier) sess.retrier = ar c.Check(ar.stopped, Equals, false) sess.autoRedial() defer sess.stopRedial() c.Check(ar.stopped, Equals, true) } func (cs *clientSessionSuite) TestAutoRedialStopsRetrier(c *C) { // checks that AutoRedial stops the previous retrier sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.doneCh = make(chan uint32) c.Check(sess.retrier, IsNil) sess.autoRedial() c.Assert(sess.retrier, NotNil) sess.retrier.Stop() c.Check(<-sess.doneCh, Not(Equals), 0) } func (cs *clientSessionSuite) TestAutoRedialCallsRedialDelay(c *C) { // NOTE there are tests that use calling redialDelay as an indication of calling autoRedial! sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) flag := false sess.redialDelay = func(sess *clientSession) time.Duration { flag = true; return 0 } sess.autoRedial() c.Check(flag, Equals, true) } func (cs *clientSessionSuite) TestAutoRedialSetsRedialDelayIfTooQuick(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.redialDelay = func(sess *clientSession) time.Duration { return 0 } sess.autoRedial() c.Check(sess.ShouldDelay(), Equals, false) sess.stopRedial() sess.clearShouldDelay() sess.autoRedial() c.Check(sess.ShouldDelay(), Equals, true) } /**************************************************************** handlePing() tests ****************************************************************/ type msgSuite struct { sess *clientSession upCh chan interface{} downCh chan interface{} } var _ = Suite(&msgSuite{}) func (s *msgSuite) SetUpTest(c *C) { var err error conf := dummyConf() conf.ExchangeTimeout = time.Millisecond s.sess, err = NewSession("", conf, "wah", seenstate.NewSeenState, helpers.NewTestLogger(c, "debug")) c.Assert(err, IsNil) s.sess.Connection = &testConn{Name: "TestHandle*"} s.upCh = make(chan interface{}, 5) s.downCh = make(chan interface{}, 5) s.sess.proto = &testProtocol{up: s.upCh, down: s.downCh} } func (s *msgSuite) TestHandlePingWorks(c *C) { s.upCh <- nil // no error c.Check(s.sess.handlePing(), IsNil) c.Assert(len(s.downCh), Equals, 1) c.Check(<-s.downCh, Equals, protocol.PingPongMsg{Type: "pong"}) } func (s *msgSuite) TestHandlePingHandlesPongWriteError(c *C) { failure := errors.New("Pong") s.upCh <- failure c.Check(s.sess.handlePing(), Equals, failure) c.Assert(len(s.downCh), Equals, 1) c.Check(<-s.downCh, Equals, protocol.PingPongMsg{Type: "pong"}) c.Check(s.sess.State(), Equals, Error) } func (s *msgSuite) TestHandlePingClearsDelay(c *C) { s.sess.setShouldDelay() s.upCh <- nil // no error c.Check(s.sess.handlePing(), IsNil) c.Assert(len(s.downCh), Equals, 1) c.Check(<-s.downCh, Equals, protocol.PingPongMsg{Type: "pong"}) c.Check(s.sess.ShouldDelay(), Equals, false) } func (s *msgSuite) TestHandlePingDoesNotClearsDelayOnError(c *C) { s.sess.setShouldDelay() s.upCh <- errors.New("Pong") c.Check(s.sess.handlePing(), NotNil) c.Assert(len(s.downCh), Equals, 1) c.Check(<-s.downCh, Equals, protocol.PingPongMsg{Type: "pong"}) c.Check(s.sess.ShouldDelay(), Equals, true) } /**************************************************************** handleBroadcast() tests ****************************************************************/ func (s *msgSuite) TestHandleBroadcastWorks(c *C) { msg := new(serverMsg) msg.Type = "broadcast" msg.BroadcastMsg = protocol.BroadcastMsg{ Type: "broadcast", AppId: "--ignored--", ChanId: "0", TopLevel: 2, Payloads: []json.RawMessage{ json.RawMessage(`{"img1/m1":[101,"tubular"]}`), json.RawMessage("false"), // shouldn't happen but robust json.RawMessage(`{"img1/m1":[102,"tubular"]}`), }, } go func() { s.sess.errCh <- s.sess.handleBroadcast(msg) }() c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) s.upCh <- nil // ack ok c.Check(<-s.sess.errCh, Equals, nil) c.Assert(len(s.sess.BroadcastCh), Equals, 1) c.Check(<-s.sess.BroadcastCh, DeepEquals, &BroadcastNotification{ TopLevel: 2, Decoded: []map[string]interface{}{ map[string]interface{}{ "img1/m1": []interface{}{float64(101), "tubular"}, }, map[string]interface{}{ "img1/m1": []interface{}{float64(102), "tubular"}, }, }, }) // and finally, the session keeps track of the levels levels, err := s.sess.SeenState.GetAllLevels() c.Check(err, IsNil) c.Check(levels, DeepEquals, map[string]int64{"0": 2}) } func (s *msgSuite) TestHandleBroadcastBadAckWrite(c *C) { msg := new(serverMsg) msg.Type = "broadcast" msg.BroadcastMsg = protocol.BroadcastMsg{ Type: "broadcast", AppId: "APP", ChanId: "0", TopLevel: 2, Payloads: []json.RawMessage{json.RawMessage(`{"b":1}`)}, } go func() { s.sess.errCh <- s.sess.handleBroadcast(msg) }() c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) failure := errors.New("ACK ACK ACK") s.upCh <- failure c.Assert(<-s.sess.errCh, Equals, failure) c.Check(s.sess.State(), Equals, Error) } func (s *msgSuite) TestHandleBroadcastWrongChannel(c *C) { msg := new(serverMsg) msg.Type = "brodacast" msg.BroadcastMsg = protocol.BroadcastMsg{ Type: "broadcast", AppId: "APP", ChanId: "something awful", TopLevel: 2, Payloads: []json.RawMessage{json.RawMessage(`{"b":1}`)}, } go func() { s.sess.errCh <- s.sess.handleBroadcast(msg) }() c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) s.upCh <- nil // ack ok c.Check(<-s.sess.errCh, IsNil) c.Check(len(s.sess.BroadcastCh), Equals, 0) } func (s *msgSuite) TestHandleBroadcastBrokenSeenState(c *C) { s.sess.SeenState = &brokenSeenState{} msg := new(serverMsg) msg.Type = "broadcast" msg.BroadcastMsg = protocol.BroadcastMsg{ Type: "broadcast", AppId: "--ignored--", ChanId: "0", TopLevel: 2, Payloads: []json.RawMessage{json.RawMessage(`{"b":1}`)}, } go func() { s.sess.errCh <- s.sess.handleBroadcast(msg) }() s.upCh <- nil // ack ok // start returns with error c.Check(<-s.sess.errCh, Not(Equals), nil) c.Check(s.sess.State(), Equals, Error) // no message sent out c.Check(len(s.sess.BroadcastCh), Equals, 0) // and nak'ed it c.Check(len(s.downCh), Equals, 1) c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"nak"}) } func (s *msgSuite) TestHandleBroadcastClearsDelay(c *C) { s.sess.setShouldDelay() msg := &serverMsg{Type: "broadcast"} go func() { s.sess.errCh <- s.sess.handleBroadcast(msg) }() c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) s.upCh <- nil // ack ok c.Check(<-s.sess.errCh, IsNil) c.Check(s.sess.ShouldDelay(), Equals, false) } func (s *msgSuite) TestHandleBroadcastDoesNotClearDelayOnError(c *C) { s.sess.setShouldDelay() msg := &serverMsg{Type: "broadcast"} go func() { s.sess.errCh <- s.sess.handleBroadcast(msg) }() c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) s.upCh <- errors.New("bcast") c.Check(<-s.sess.errCh, NotNil) c.Check(s.sess.ShouldDelay(), Equals, true) } /**************************************************************** handleNotifications() tests ****************************************************************/ type testAddresseeChecking struct { ops chan string missing string } func (ac *testAddresseeChecking) StartAddresseeBatch() { ac.ops <- "start" } func (ac *testAddresseeChecking) CheckForAddressee(notif *protocol.Notification) *click.AppId { ac.ops <- notif.AppId if notif.AppId != ac.missing { id, err := click.ParseAppId(notif.AppId) if err != nil { panic(err) } return id } else { return nil } } func (s *msgSuite) TestHandleNotificationsWorks(c *C) { ac := &testAddresseeChecking{ops: make(chan string, 10)} s.sess.AddresseeChecker = ac s.sess.setShouldDelay() n1 := protocol.Notification{ AppId: "com.example.app1_app1", MsgId: "a", Payload: json.RawMessage(`{"m": 1}`), } n2 := protocol.Notification{ AppId: "com.example.app2_app2", MsgId: "b", Payload: json.RawMessage(`{"m": 2}`), } msg := new(serverMsg) msg.Type = "notifications" msg.NotificationsMsg = protocol.NotificationsMsg{ Notifications: []protocol.Notification{n1, n2}, } go func() { s.sess.errCh <- s.sess.handleNotifications(msg) }() c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) s.upCh <- nil // ack ok c.Check(<-s.sess.errCh, Equals, nil) c.Check(s.sess.ShouldDelay(), Equals, false) c.Assert(s.sess.NotificationsCh, HasLen, 2) app1, err := click.ParseAppId("com.example.app1_app1") c.Assert(err, IsNil) c.Check(<-s.sess.NotificationsCh, DeepEquals, AddressedNotification{ To: app1, Notification: &n1, }) app2, err := click.ParseAppId("com.example.app2_app2") c.Assert(err, IsNil) c.Check(<-s.sess.NotificationsCh, DeepEquals, AddressedNotification{ To: app2, Notification: &n2, }) c.Check(ac.ops, HasLen, 3) c.Check(<-ac.ops, Equals, "start") c.Check(<-ac.ops, Equals, "com.example.app1_app1") c.Check(<-ac.ops, Equals, "com.example.app2_app2") } func (s *msgSuite) TestHandleNotificationsAddresseeCheck(c *C) { ac := &testAddresseeChecking{ ops: make(chan string, 10), missing: "com.example.app1_app1", } s.sess.AddresseeChecker = ac s.sess.setShouldDelay() n1 := protocol.Notification{ AppId: "com.example.app1_app1", MsgId: "a", Payload: json.RawMessage(`{"m": 1}`), } n2 := protocol.Notification{ AppId: "com.example.app2_app2", MsgId: "b", Payload: json.RawMessage(`{"m": 2}`), } msg := new(serverMsg) msg.Type = "notifications" msg.NotificationsMsg = protocol.NotificationsMsg{ Notifications: []protocol.Notification{n1, n2}, } go func() { s.sess.errCh <- s.sess.handleNotifications(msg) }() c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) s.upCh <- nil // ack ok c.Check(<-s.sess.errCh, Equals, nil) c.Check(s.sess.ShouldDelay(), Equals, false) c.Assert(s.sess.NotificationsCh, HasLen, 1) app2, err := click.ParseAppId("com.example.app2_app2") c.Assert(err, IsNil) c.Check(<-s.sess.NotificationsCh, DeepEquals, AddressedNotification{ To: app2, Notification: &n2, }) c.Check(ac.ops, HasLen, 3) c.Check(<-ac.ops, Equals, "start") c.Check(<-ac.ops, Equals, "com.example.app1_app1") } func (s *msgSuite) TestHandleNotificationsFiltersSeen(c *C) { ac := &testAddresseeChecking{ops: make(chan string, 10)} s.sess.AddresseeChecker = ac n1 := protocol.Notification{ AppId: "com.example.app1_app1", MsgId: "a", Payload: json.RawMessage(`{"m": 1}`), } n2 := protocol.Notification{ AppId: "com.example.app2_app2", MsgId: "b", Payload: json.RawMessage(`{"m": 2}`), } msg := new(serverMsg) msg.Type = "notifications" msg.NotificationsMsg = protocol.NotificationsMsg{ Notifications: []protocol.Notification{n1, n2}, } go func() { s.sess.errCh <- s.sess.handleNotifications(msg) }() c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) s.upCh <- nil // ack ok c.Check(<-s.sess.errCh, Equals, nil) c.Assert(s.sess.NotificationsCh, HasLen, 2) app1, err := click.ParseAppId("com.example.app1_app1") c.Assert(err, IsNil) c.Check(<-s.sess.NotificationsCh, DeepEquals, AddressedNotification{ To: app1, Notification: &n1, }) app2, err := click.ParseAppId("com.example.app2_app2") c.Assert(err, IsNil) c.Check(<-s.sess.NotificationsCh, DeepEquals, AddressedNotification{ To: app2, Notification: &n2, }) c.Check(ac.ops, HasLen, 3) // second time they get ignored go func() { s.sess.errCh <- s.sess.handleNotifications(msg) }() c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) s.upCh <- nil // ack ok c.Check(<-s.sess.errCh, Equals, nil) c.Assert(s.sess.NotificationsCh, HasLen, 0) c.Check(ac.ops, HasLen, 4) } func (s *msgSuite) TestHandleNotificationsBadAckWrite(c *C) { s.sess.setShouldDelay() n1 := protocol.Notification{ AppId: "com.example.app1_app1", MsgId: "a", Payload: json.RawMessage(`{"m": 1}`), } msg := new(serverMsg) msg.Type = "notifications" msg.NotificationsMsg = protocol.NotificationsMsg{ Notifications: []protocol.Notification{n1}, } go func() { s.sess.errCh <- s.sess.handleNotifications(msg) }() c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) failure := errors.New("ACK ACK ACK") s.upCh <- failure c.Assert(<-s.sess.errCh, Equals, failure) c.Check(s.sess.State(), Equals, Error) // didn't get to clear c.Check(s.sess.ShouldDelay(), Equals, true) } func (s *msgSuite) TestHandleNotificationsBrokenSeenState(c *C) { s.sess.setShouldDelay() s.sess.SeenState = &brokenSeenState{} n1 := protocol.Notification{ AppId: "com.example.app1_app1", MsgId: "a", Payload: json.RawMessage(`{"m": 1}`), } msg := new(serverMsg) msg.Type = "notifications" msg.NotificationsMsg = protocol.NotificationsMsg{ Notifications: []protocol.Notification{n1}, } go func() { s.sess.errCh <- s.sess.handleNotifications(msg) }() s.upCh <- nil // ack ok // start returns with error c.Check(<-s.sess.errCh, Not(Equals), nil) c.Check(s.sess.State(), Equals, Error) // no message sent out c.Check(len(s.sess.NotificationsCh), Equals, 0) // and nak'ed it c.Check(len(s.downCh), Equals, 1) c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"nak"}) // didn't get to clear c.Check(s.sess.ShouldDelay(), Equals, true) } /**************************************************************** handleConnBroken() tests ****************************************************************/ func (s *msgSuite) TestHandleConnBrokenUnkwown(c *C) { msg := new(serverMsg) msg.Type = "connbroken" msg.ConnBrokenMsg = protocol.ConnBrokenMsg{ Reason: "REASON", } go func() { s.sess.errCh <- s.sess.handleConnBroken(msg) }() c.Check(<-s.sess.errCh, ErrorMatches, "server broke connection: REASON") c.Check(s.sess.State(), Equals, Error) } func (s *msgSuite) TestHandleConnBrokenHostMismatch(c *C) { msg := new(serverMsg) msg.Type = "connbroken" msg.ConnBrokenMsg = protocol.ConnBrokenMsg{ Reason: protocol.BrokenHostMismatch, } s.sess.deliveryHosts = []string{"foo:443", "bar:443"} go func() { s.sess.errCh <- s.sess.handleConnBroken(msg) }() c.Check(<-s.sess.errCh, ErrorMatches, "server broke connection: host-mismatch") c.Check(s.sess.State(), Equals, Error) // hosts were reset c.Check(s.sess.deliveryHosts, IsNil) } /**************************************************************** loop() tests ****************************************************************/ type loopSuite msgSuite var _ = Suite(&loopSuite{}) func (s *loopSuite) SetUpTest(c *C) { (*msgSuite)(s).SetUpTest(c) s.sess.Connection.(*testConn).Name = "TestLoop*" go func() { s.sess.errCh <- s.sess.loop() }() } func (s *loopSuite) waitUntilRunning(c *C) { delay := time.Duration(1000) for i := 0; i < 5; i++ { if s.sess.State() == Running { return } time.Sleep(delay) delay *= 2 } c.Check(s.sess.State(), Equals, Running) } func (s *loopSuite) TestLoopReadError(c *C) { s.waitUntilRunning(c) s.upCh <- errors.New("Read") err := <-s.sess.errCh c.Check(err, ErrorMatches, "Read") c.Check(s.sess.State(), Equals, Error) } func (s *loopSuite) TestLoopPing(c *C) { s.waitUntilRunning(c) c.Check(takeNext(s.downCh), Equals, "deadline 1ms") s.upCh <- protocol.PingPongMsg{Type: "ping"} c.Check(takeNext(s.downCh), Equals, protocol.PingPongMsg{Type: "pong"}) failure := errors.New("pong") s.upCh <- failure c.Check(<-s.sess.errCh, Equals, failure) } func (s *loopSuite) TestLoopLoopsDaLoop(c *C) { s.waitUntilRunning(c) for i := 1; i < 10; i++ { c.Check(takeNext(s.downCh), Equals, "deadline 1ms") s.upCh <- protocol.PingPongMsg{Type: "ping"} c.Check(takeNext(s.downCh), Equals, protocol.PingPongMsg{Type: "pong"}) s.upCh <- nil } failure := errors.New("pong") s.upCh <- failure c.Check(<-s.sess.errCh, Equals, failure) } func (s *loopSuite) TestLoopBroadcast(c *C) { s.waitUntilRunning(c) b := &protocol.BroadcastMsg{ Type: "broadcast", AppId: "--ignored--", ChanId: "0", TopLevel: 2, Payloads: []json.RawMessage{json.RawMessage(`{"b":1}`)}, } c.Check(takeNext(s.downCh), Equals, "deadline 1ms") s.upCh <- b c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) failure := errors.New("ack") s.upCh <- failure c.Check(<-s.sess.errCh, Equals, failure) } func (s *loopSuite) TestLoopNotifications(c *C) { s.waitUntilRunning(c) n1 := protocol.Notification{ AppId: "app1", MsgId: "a", Payload: json.RawMessage(`{"m": 1}`), } msg := &protocol.NotificationsMsg{ Type: "notifications", Notifications: []protocol.Notification{n1}, } c.Check(takeNext(s.downCh), Equals, "deadline 1ms") s.upCh <- msg c.Check(takeNext(s.downCh), Equals, protocol.AckMsg{"ack"}) failure := errors.New("ack") s.upCh <- failure c.Check(<-s.sess.errCh, Equals, failure) } func (s *loopSuite) TestLoopSetParams(c *C) { s.waitUntilRunning(c) setParams := protocol.SetParamsMsg{ Type: "setparams", SetCookie: "COOKIE", } c.Check(takeNext(s.downCh), Equals, "deadline 1ms") s.upCh <- setParams failure := errors.New("fail") s.upCh <- failure c.Assert(<-s.sess.errCh, Equals, failure) c.Check(s.sess.getCookie(), Equals, "COOKIE") } func (s *loopSuite) TestLoopConnBroken(c *C) { s.waitUntilRunning(c) broken := protocol.ConnBrokenMsg{ Type: "connbroken", Reason: "REASON", } c.Check(takeNext(s.downCh), Equals, "deadline 1ms") s.upCh <- broken c.Check(<-s.sess.errCh, NotNil) } func (s *loopSuite) TestLoopConnWarn(c *C) { warn := protocol.ConnWarnMsg{ Type: "warn", Reason: "XXX", } connwarn := protocol.ConnWarnMsg{ Type: "connwarn", Reason: "REASON", } failure := errors.New("warn") log := s.sess.Log.(*helpers.TestLogger) s.waitUntilRunning(c) c.Check(takeNext(s.downCh), Equals, "deadline 1ms") log.ResetCapture() s.upCh <- warn s.upCh <- connwarn s.upCh <- failure c.Check(<-s.sess.errCh, Equals, failure) c.Check(log.Captured(), Matches, `(?ms).* warning: XXX$.*`) c.Check(log.Captured(), Matches, `(?ms).* warning: REASON$`) } /**************************************************************** start() tests ****************************************************************/ func (cs *clientSessionSuite) TestStartFailsIfSetDeadlineFails(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.Connection = &testConn{Name: "TestStartFailsIfSetDeadlineFails", DeadlineCondition: condition.Work(false)} // setdeadline will fail err = sess.start() c.Check(err, ErrorMatches, ".*deadline.*") c.Check(sess.State(), Equals, Error) } func (cs *clientSessionSuite) TestStartFailsIfWriteFails(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.Connection = &testConn{Name: "TestStartFailsIfWriteFails", WriteCondition: condition.Work(false)} // write will fail err = sess.start() c.Check(err, ErrorMatches, ".*write.*") c.Check(sess.State(), Equals, Error) } func (cs *clientSessionSuite) TestStartFailsIfGetLevelsFails(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.SeenState = &brokenSeenState{} sess.Connection = &testConn{Name: "TestStartConnectMessageFails"} errCh := make(chan error, 1) upCh := make(chan interface{}, 5) downCh := make(chan interface{}, 5) proto := &testProtocol{up: upCh, down: downCh} sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto } go func() { errCh <- sess.start() }() c.Check(takeNext(downCh), Equals, "deadline 0") err = <-errCh c.Check(err, ErrorMatches, "broken.") } func (cs *clientSessionSuite) TestStartConnectMessageFails(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.Connection = &testConn{Name: "TestStartConnectMessageFails"} errCh := make(chan error, 1) upCh := make(chan interface{}, 5) downCh := make(chan interface{}, 5) proto := &testProtocol{up: upCh, down: downCh} sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto } go func() { errCh <- sess.start() }() c.Check(takeNext(downCh), Equals, "deadline 0") c.Check(takeNext(downCh), DeepEquals, protocol.ConnectMsg{ Type: "connect", DeviceId: sess.DeviceId, Levels: map[string]int64{}, Authorization: "", }) upCh <- errors.New("Overflow error in /dev/null") err = <-errCh c.Check(err, ErrorMatches, "Overflow.*null") c.Check(sess.State(), Equals, Error) } func (cs *clientSessionSuite) TestStartConnackReadError(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.Connection = &testConn{Name: "TestStartConnackReadError"} errCh := make(chan error, 1) upCh := make(chan interface{}, 5) downCh := make(chan interface{}, 5) proto := &testProtocol{up: upCh, down: downCh} sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto } go func() { errCh <- sess.start() }() c.Check(takeNext(downCh), Equals, "deadline 0") _, ok := takeNext(downCh).(protocol.ConnectMsg) c.Check(ok, Equals, true) upCh <- nil // no error upCh <- io.EOF err = <-errCh c.Check(err, ErrorMatches, ".*EOF.*") c.Check(sess.State(), Equals, Error) } func (cs *clientSessionSuite) TestStartBadConnack(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.Connection = &testConn{Name: "TestStartBadConnack"} errCh := make(chan error, 1) upCh := make(chan interface{}, 5) downCh := make(chan interface{}, 5) proto := &testProtocol{up: upCh, down: downCh} sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto } go func() { errCh <- sess.start() }() c.Check(takeNext(downCh), Equals, "deadline 0") _, ok := takeNext(downCh).(protocol.ConnectMsg) c.Check(ok, Equals, true) upCh <- nil // no error upCh <- protocol.ConnAckMsg{Type: "connack"} err = <-errCh c.Check(err, ErrorMatches, ".*invalid.*") c.Check(sess.State(), Equals, Error) } func (cs *clientSessionSuite) TestStartNotConnack(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.Connection = &testConn{Name: "TestStartBadConnack"} errCh := make(chan error, 1) upCh := make(chan interface{}, 5) downCh := make(chan interface{}, 5) proto := &testProtocol{up: upCh, down: downCh} sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto } go func() { errCh <- sess.start() }() c.Check(takeNext(downCh), Equals, "deadline 0") _, ok := takeNext(downCh).(protocol.ConnectMsg) c.Check(ok, Equals, true) upCh <- nil // no error upCh <- protocol.ConnAckMsg{Type: "connnak"} err = <-errCh c.Check(err, ErrorMatches, ".*CONNACK.*") c.Check(sess.State(), Equals, Error) } func (cs *clientSessionSuite) TestStartWorks(c *C) { info := map[string]interface{}{ "foo": 1, "bar": "baz", } conf := ClientSessionConfig{ Info: info, } sess, err := NewSession("", conf, "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.Connection = &testConn{Name: "TestStartWorks"} sess.setCookie("COOKIE") errCh := make(chan error, 1) upCh := make(chan interface{}, 5) downCh := make(chan interface{}, 5) proto := &testProtocol{up: upCh, down: downCh} sess.Protocolator = func(_ net.Conn) protocol.Protocol { return proto } go func() { errCh <- sess.start() }() c.Check(takeNext(downCh), Equals, "deadline 0") msg, ok := takeNext(downCh).(protocol.ConnectMsg) c.Check(ok, Equals, true) c.Check(msg.DeviceId, Equals, "wah") c.Check(msg.Authorization, Equals, "") c.Check(msg.Cookie, Equals, "COOKIE") c.Check(msg.Info, DeepEquals, info) upCh <- nil // no error upCh <- protocol.ConnAckMsg{ Type: "connack", Params: protocol.ConnAckParams{(10 * time.Millisecond).String()}, } // start is now done. err = <-errCh c.Check(err, IsNil) c.Check(sess.State(), Equals, Started) } /**************************************************************** run() tests ****************************************************************/ func (cs *clientSessionSuite) TestRunCallsCloserWithFalse(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) failure := errors.New("bail") has_closed := false with_false := false err = sess.run( func(b bool) { has_closed = true; with_false = !b }, func() error { return failure }, nil, nil, nil, nil) c.Check(err, Equals, failure) c.Check(has_closed, Equals, true) c.Check(with_false, Equals, true) } func (cs *clientSessionSuite) TestRunBailsIfAuthCheckFails(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) failure := errors.New("TestRunBailsIfAuthCheckFails") has_closed := false err = sess.run( func(bool) { has_closed = true }, func() error { return failure }, nil, nil, nil, nil) c.Check(err, Equals, failure) c.Check(has_closed, Equals, true) } func (cs *clientSessionSuite) TestRunBailsIfHostGetterFails(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) failure := errors.New("TestRunBailsIfHostGetterFails") has_closed := false err = sess.run( func(bool) { has_closed = true }, func() error { return nil }, func() error { return failure }, nil, nil, nil) c.Check(err, Equals, failure) c.Check(has_closed, Equals, true) } func (cs *clientSessionSuite) TestRunBailsIfConnectFails(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) failure := errors.New("TestRunBailsIfConnectFails") err = sess.run( func(bool) {}, func() error { return nil }, func() error { return nil }, func() error { return failure }, nil, nil) c.Check(err, Equals, failure) } func (cs *clientSessionSuite) TestRunBailsIfStartFails(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) failure := errors.New("TestRunBailsIfStartFails") err = sess.run( func(bool) {}, func() error { return nil }, func() error { return nil }, func() error { return nil }, func() error { return failure }, nil) c.Check(err, Equals, failure) } func (cs *clientSessionSuite) TestRunRunsEvenIfLoopFails(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) failureCh := make(chan error) // must be unbuffered notf := &BroadcastNotification{} err = sess.run( func(bool) {}, func() error { return nil }, func() error { return nil }, func() error { return nil }, func() error { return nil }, func() error { sess.BroadcastCh <- notf; return <-failureCh }) c.Check(err, Equals, nil) // if run doesn't error it sets up the channels c.Assert(sess.errCh, NotNil) c.Assert(sess.BroadcastCh, NotNil) c.Check(<-sess.BroadcastCh, Equals, notf) failure := errors.New("TestRunRunsEvenIfLoopFails") failureCh <- failure c.Check(<-sess.errCh, Equals, failure) // so now you know it was running in a goroutine :) } /**************************************************************** Jitter() tests ****************************************************************/ func (cs *clientSessionSuite) TestJitter(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) num_tries := 20 // should do the math spread := time.Second // has_neg := false has_pos := false has_zero := true for i := 0; i < num_tries; i++ { n := sess.Jitter(spread) if n > 0 { has_pos = true } else if n < 0 { has_neg = true } else { has_zero = true } } c.Check(has_neg, Equals, true) c.Check(has_pos, Equals, true) c.Check(has_zero, Equals, true) // a negative spread is caught in the reasonable place c.Check(func() { sess.Jitter(time.Duration(-1)) }, PanicMatches, "spread must be non-negative") } /**************************************************************** Dial() tests ****************************************************************/ func (cs *clientSessionSuite) TestDialPanics(c *C) { // one last unhappy test sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.Protocolator = nil c.Check(sess.Dial, PanicMatches, ".*protocol constructor.") } var ( dialTestTimeout = 300 * time.Millisecond ) func dialTestConf(certPEM []byte) ClientSessionConfig { conf := dummyConf() conf.ExchangeTimeout = dialTestTimeout if certPEM == nil { conf.PEM = helpers.TestCertPEMBlock } else { conf.PEM = certPEM } return conf } func (cs *clientSessionSuite) TestDialBadServerName(c *C) { // a borked server name lst, err := tls.Listen("tcp", "localhost:0", helpers.TestTLSServerConfig) c.Assert(err, IsNil) // advertise ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, err := json.Marshal(map[string]interface{}{ "domain": "xyzzy", // <-- *** THIS *** is the bit that'll break it "hosts": []string{"nowhere", lst.Addr().String()}, }) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") w.Write(b) })) defer ts.Close() sess, err := NewSession(ts.URL, dialTestConf(nil), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) tconn := &testConn{} sess.Connection = tconn upCh := make(chan interface{}, 5) downCh := make(chan interface{}, 5) errCh := make(chan error, 1) proto := &testProtocol{up: upCh, down: downCh} sess.Protocolator = func(net.Conn) protocol.Protocol { return proto } go func() { errCh <- sess.Dial() }() srv, err := lst.Accept() c.Assert(err, IsNil) // connect done _, err = protocol.ReadWireFormatVersion(srv, dialTestTimeout) c.Check(err, NotNil) c.Check(<-errCh, NotNil) c.Check(sess.State(), Equals, Error) } func (cs *clientSessionSuite) TestDialWorks(c *C) { // happy path thoughts lst, err := tls.Listen("tcp", "localhost:0", helpers.TestTLSServerConfig) c.Assert(err, IsNil) // advertise ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, err := json.Marshal(map[string]interface{}{ "domain": "push-delivery", "hosts": []string{"nowhere", lst.Addr().String()}, }) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") w.Write(b) })) defer ts.Close() sess, err := NewSession(ts.URL, dialTestConf(nil), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) tconn := &testConn{CloseCondition: condition.Fail2Work(10)} sess.Connection = tconn // just to be sure: c.Check(tconn.CloseCondition.String(), Matches, ".* 10 to go.") upCh := make(chan interface{}, 5) downCh := make(chan interface{}, 5) proto := &testProtocol{up: upCh, down: downCh} sess.Protocolator = func(net.Conn) protocol.Protocol { return proto } go sess.Dial() srv, err := lst.Accept() c.Assert(err, IsNil) // connect done // Dial should have had the session's old connection (tconn) closed // before connecting a new one; if that was done, tconn's condition // ticked forward: c.Check(tconn.CloseCondition.String(), Matches, ".* 9 to go.") // now, start: 1. protocol version v, err := protocol.ReadWireFormatVersion(srv, dialTestTimeout) c.Assert(err, IsNil) c.Assert(v, Equals, protocol.ProtocolWireVersion) // if something goes wrong session would try the first/other host c.Check(sess.tryHost, Equals, 0) // 2. "connect" (but on the fake protcol above! woo) c.Check(takeNext(downCh), Equals, fmt.Sprintf("deadline %v", dialTestTimeout)) _, ok := takeNext(downCh).(protocol.ConnectMsg) c.Check(ok, Equals, true) upCh <- nil // no error upCh <- protocol.ConnAckMsg{ Type: "connack", Params: protocol.ConnAckParams{(10 * time.Millisecond).String()}, } // start is now done. // 3. "loop" // ping works, c.Check(takeNext(downCh), Equals, fmt.Sprintf("deadline %v", dialTestTimeout+10*time.Millisecond)) upCh <- protocol.PingPongMsg{Type: "ping"} c.Check(takeNext(downCh), Equals, protocol.PingPongMsg{Type: "pong"}) upCh <- nil // session would retry the same host c.Check(sess.tryHost, Equals, 1) // and broadcasts... b := &protocol.BroadcastMsg{ Type: "broadcast", AppId: "--ignored--", ChanId: "0", TopLevel: 2, Payloads: []json.RawMessage{json.RawMessage(`{"b":1}`)}, } c.Check(takeNext(downCh), Equals, fmt.Sprintf("deadline %v", dialTestTimeout+10*time.Millisecond)) upCh <- b c.Check(takeNext(downCh), Equals, protocol.AckMsg{"ack"}) upCh <- nil // ...get bubbled up, c.Check(<-sess.BroadcastCh, NotNil) // and their TopLevel remembered levels, err := sess.SeenState.GetAllLevels() c.Check(err, IsNil) c.Check(levels, DeepEquals, map[string]int64{"0": 2}) // and ping still work even after that. c.Check(takeNext(downCh), Equals, fmt.Sprintf("deadline %v", dialTestTimeout+10*time.Millisecond)) upCh <- protocol.PingPongMsg{Type: "ping"} c.Check(takeNext(downCh), Equals, protocol.PingPongMsg{Type: "pong"}) failure := errors.New("pongs") upCh <- failure c.Check(<-sess.errCh, Equals, failure) } func (cs *clientSessionSuite) TestDialWorksDirect(c *C) { // happy path thoughts lst, err := tls.Listen("tcp", "localhost:0", helpers.TestTLSServerConfig) c.Assert(err, IsNil) sess, err := NewSession(lst.Addr().String(), dialTestConf(nil), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) defer sess.StopKeepConnection() upCh := make(chan interface{}, 5) downCh := make(chan interface{}, 5) proto := &testProtocol{up: upCh, down: downCh} sess.Protocolator = func(net.Conn) protocol.Protocol { return proto } go sess.Dial() cli, err := lst.Accept() c.Assert(err, IsNil) cli.SetReadDeadline(time.Now().Add(2 * time.Second)) var buf [1]byte _, err = cli.Read(buf[:]) c.Assert(err, IsNil) // connect done } func (cs *clientSessionSuite) TestDialWorksDirectSHA512Cert(c *C) { // happy path thoughts lst, err := tls.Listen("tcp", "localhost:0", helpers.TestTLSServerConfigs["sha512"]) c.Assert(err, IsNil) sess, err := NewSession(lst.Addr().String(), dialTestConf(helpers.TestCertPEMBlock512), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) defer sess.StopKeepConnection() upCh := make(chan interface{}, 5) downCh := make(chan interface{}, 5) proto := &testProtocol{up: upCh, down: downCh} sess.Protocolator = func(conn net.Conn) protocol.Protocol { return proto } go sess.Dial() cli, err := lst.Accept() c.Assert(err, IsNil) cli.SetReadDeadline(time.Now().Add(2 * time.Second)) var buf [1]byte _, err = cli.Read(buf[:]) c.Assert(err, IsNil) // connect done } /**************************************************************** redialDelay() tests ****************************************************************/ func (cs *clientSessionSuite) TestShouldDelay(c *C) { sess, err := NewSession("foo:443", dummyConf(), "", cs.lvls, cs.log) c.Assert(err, IsNil) c.Check(sess.ShouldDelay(), Equals, false) sess.setShouldDelay() c.Check(sess.ShouldDelay(), Equals, true) sess.clearShouldDelay() c.Check(sess.ShouldDelay(), Equals, false) } func (cs *clientSessionSuite) TestRedialDelay(c *C) { sess, err := NewSession("foo:443", dummyConf(), "", cs.lvls, cs.log) c.Assert(err, IsNil) sess.redialDelays = []time.Duration{17, 42} n := 0 sess.redialJitter = func(time.Duration) time.Duration { n++; return 0 } // we get increasing delays while we're unhappy sess.setShouldDelay() c.Check(redialDelay(sess), Equals, time.Duration(17)) c.Check(redialDelay(sess), Equals, time.Duration(42)) c.Check(redialDelay(sess), Equals, time.Duration(42)) // once we're happy, delays drop to 0 sess.clearShouldDelay() c.Check(redialDelay(sess), Equals, time.Duration(0)) // and start again from the top if we become unhappy again sess.setShouldDelay() c.Check(redialDelay(sess), Equals, time.Duration(17)) // and redialJitter got called every time shouldDelay was true c.Check(n, Equals, 4) } /**************************************************************** ResetCookie() tests ****************************************************************/ func (cs *clientSessionSuite) TestResetCookie(c *C) { sess, err := NewSession("foo:443", dummyConf(), "", cs.lvls, cs.log) c.Assert(err, IsNil) c.Assert(sess.KeepConnection(), IsNil) defer sess.StopKeepConnection() c.Check(sess.getCookie(), Equals, "") sess.setCookie("COOKIE") c.Check(sess.getCookie(), Equals, "COOKIE") sess.ResetCookie() c.Check(sess.getCookie(), Equals, "") } /**************************************************************** KeepConnection() (and related) tests ****************************************************************/ func (cs *clientSessionSuite) TestKeepConnectionDoesNothingIfNotConnected(c *C) { // how do you test "does nothing?" sess, err := NewSession("foo:443", dummyConf(), "", cs.lvls, cs.log) c.Assert(err, IsNil) c.Assert(sess, NotNil) c.Assert(sess.State(), Equals, Pristine) c.Assert(sess.KeepConnection(), IsNil) defer sess.StopKeepConnection() // stopCh is meant to be used just for closing it, but abusing // it for testing seems the right thing to do: this ensures // the thing is ticking along before we check the state of // stuff. sess.stopCh <- struct{}{} c.Check(sess.State(), Equals, Disconnected) } func (cs *clientSessionSuite) TestYouCantCallKeepConnectionTwice(c *C) { sess, err := NewSession("foo:443", dummyConf(), "", cs.lvls, cs.log) c.Assert(err, IsNil) c.Assert(sess, NotNil) c.Assert(sess.State(), Equals, Pristine) c.Assert(sess.KeepConnection(), IsNil) defer sess.StopKeepConnection() c.Check(sess.KeepConnection(), NotNil) } func (cs *clientSessionSuite) TestStopKeepConnectionShutsdown(c *C) { sess, err := NewSession("foo:443", dummyConf(), "", cs.lvls, cs.log) c.Assert(err, IsNil) c.Assert(sess, NotNil) sess.StopKeepConnection() c.Check(sess.State(), Equals, Shutdown) } func (cs *clientSessionSuite) TestHasConnectivityTriggersConnectivityHandler(c *C) { sess, err := NewSession("foo:443", dummyConf(), "", cs.lvls, cs.log) c.Assert(err, IsNil) c.Assert(sess, NotNil) testCh := make(chan bool) sess.connHandler = func(p bool) { testCh <- p } go sess.doKeepConnection() defer sess.StopKeepConnection() sess.HasConnectivity(true) c.Check(<-testCh, Equals, true) sess.HasConnectivity(false) c.Check(<-testCh, Equals, false) } func (cs *clientSessionSuite) TestDoneChIsEmptiedAndLogged(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) sess.doneCh = make(chan uint32) // unbuffered sess.KeepConnection() defer sess.StopKeepConnection() sess.doneCh <- 23 sess.doneCh <- 24 // makes sure the first one has been processed before checking c.Check(cs.log.Captured(), Matches, `(?ms).* connected after 23 attempts\.`) } func (cs *clientSessionSuite) TestErrChIsEmptiedAndLoggedAndAutoRedial(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) ch := make(chan struct{}, 1) sess.errCh = make(chan error) // unbuffered sess.redialDelay = func(sess *clientSession) time.Duration { ch <- struct{}{}; return 0 } sess.lastConn = true // -> autoRedial, if the session is in Disconnected sess.KeepConnection() defer sess.StopKeepConnection() sess.setState(Error) sess.errCh <- errors.New("potato") select { case <-ch: // all ok case <-time.After(100 * time.Millisecond): c.Fatalf("redialDelay not called (-> autoRedial not called)?") } c.Check(cs.log.Captured(), Matches, `(?ms).* session error.*potato`) } func (cs *clientSessionSuite) TestErrChIsEmptiedAndLoggedNoAutoRedial(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) ch := make(chan struct{}, 1) sess.errCh = make(chan error) // unbuffered sess.redialDelay = func(sess *clientSession) time.Duration { ch <- struct{}{}; return 0 } sess.connHandler = func(bool) {} sess.lastConn = false // so, no autoredial sess.KeepConnection() defer sess.StopKeepConnection() sess.errCh <- errors.New("potato") c.Assert(sess.State(), Equals, Disconnected) select { case <-ch: c.Fatalf("redialDelay called (-> autoRedial called) when disconnected?") case <-time.After(100 * time.Millisecond): // all ok } c.Check(cs.log.Captured(), Matches, `(?ms).* session error.*potato`) } func (cs *clientSessionSuite) TestHandleConnConnFromConnected(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) ch := make(chan struct{}, 1) sess.redialDelay = func(sess *clientSession) time.Duration { ch <- struct{}{}; return 0 } sess.state = Connected sess.lastConn = true sess.handleConn(true) c.Check(sess.lastConn, Equals, true) select { case <-ch: // all ok case <-time.After(100 * time.Millisecond): c.Fatalf("redialDelay not called (-> autoRedial not called)?") } } func (cs *clientSessionSuite) TestHandleConnConnFromDisconnected(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) ch := make(chan struct{}, 1) sess.redialDelay = func(sess *clientSession) time.Duration { ch <- struct{}{}; return 0 } sess.state = Disconnected sess.lastConn = false sess.handleConn(true) c.Check(sess.lastConn, Equals, true) select { case <-ch: // all ok case <-time.After(100 * time.Millisecond): c.Fatalf("redialDelay not called (-> autoRedial not called)?") } } func (cs *clientSessionSuite) TestHandleConnNotConnFromDisconnected(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) ch := make(chan struct{}, 1) sess.redialDelay = func(sess *clientSession) time.Duration { ch <- struct{}{}; return 0 } sess.state = Disconnected sess.lastConn = false sess.handleConn(false) c.Check(sess.lastConn, Equals, false) select { case <-ch: c.Fatalf("redialDelay called (-> autoRedial called)?") case <-time.After(100 * time.Millisecond): // all ok } c.Check(cs.log.Captured(), Matches, `(?ms).*-> Disconnected`) } func (cs *clientSessionSuite) TestHandleConnNotConnFromConnected(c *C) { sess, err := NewSession("", dummyConf(), "wah", cs.lvls, cs.log) c.Assert(err, IsNil) ch := make(chan struct{}, 1) sess.redialDelay = func(sess *clientSession) time.Duration { ch <- struct{}{}; return 0 } sess.state = Connected sess.lastConn = true sess.handleConn(false) c.Check(sess.lastConn, Equals, false) select { case <-ch: c.Fatalf("redialDelay called (-> autoRedial called)?") case <-time.After(100 * time.Millisecond): // all ok } c.Check(cs.log.Captured(), Matches, `(?ms).*-> Disconnected`) } ubuntu-push-0.68+16.04.20160310.2/client/session/session.go0000644000015600001650000005351712670364255023443 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ // Package session handles the minutiae of interacting with // the Ubuntu Push Notifications server. package session import ( _ "crypto/sha512" // support sha384/512 certs "crypto/tls" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "math/rand" "net" "strings" "sync" "sync/atomic" "time" "launchpad.net/ubuntu-push/click" "launchpad.net/ubuntu-push/client/gethosts" "launchpad.net/ubuntu-push/client/session/seenstate" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/protocol" "launchpad.net/ubuntu-push/util" ) type sessCmd uint8 const ( cmdDisconnect sessCmd = iota cmdConnect cmdResetCookie ) var ( wireVersionBytes = []byte{protocol.ProtocolWireVersion} ) type BroadcastNotification struct { TopLevel int64 Decoded []map[string]interface{} } type serverMsg struct { Type string `json:"T"` protocol.BroadcastMsg protocol.NotificationsMsg protocol.ConnBrokenMsg protocol.SetParamsMsg } // parseServerAddrSpec recognizes whether spec is a HTTP URL to get // hosts from or a |-separated list of host:port pairs. func parseServerAddrSpec(spec string) (hostsEndpoint string, fallbackHosts []string) { if strings.HasPrefix(spec, "http") { return spec, nil } return "", strings.Split(spec, "|") } // ClientSessionState is a way to broadly track the progress of the session type ClientSessionState uint32 const ( Error ClientSessionState = iota Pristine Disconnected Connected Started Running Shutdown Unknown ) func (s ClientSessionState) String() string { if s >= Unknown { return fmt.Sprintf("??? (%d)", s) } return [Unknown]string{ "Error", "Pristine", "Disconnected", "Connected", "Started", "Running", "Shutdown", }[s] } type hostGetter interface { Get() (*gethosts.Host, error) } // AddresseeChecking can check if a notification can be delivered. type AddresseeChecking interface { StartAddresseeBatch() CheckForAddressee(*protocol.Notification) *click.AppId } // AddressedNotification carries both a protocol.Notification and a parsed // AppId addressee. type AddressedNotification struct { To *click.AppId Notification *protocol.Notification } // ClientSessionConfig groups the client session configuration. type ClientSessionConfig struct { ConnectTimeout time.Duration ExchangeTimeout time.Duration HostsCachingExpiryTime time.Duration ExpectAllRepairedTime time.Duration PEM []byte Info map[string]interface{} AuthGetter func(string) string AuthURL string AddresseeChecker AddresseeChecking BroadcastCh chan *BroadcastNotification NotificationsCh chan AddressedNotification } // ClientSession holds a client<->server session and its configuration. type ClientSession interface { ResetCookie() State() ClientSessionState HasConnectivity(bool) KeepConnection() error StopKeepConnection() } type clientSession struct { // configuration DeviceId string ClientSessionConfig SeenState seenstate.SeenState Protocolator func(net.Conn) protocol.Protocol // hosts getHost hostGetter fallbackHosts []string deliveryHostsTimestamp time.Time deliveryHosts []string lastAttemptTimestamp time.Time leftToTry int tryHost int // hook for testing timeSince func(time.Time) time.Duration // connection connLock sync.RWMutex Connection net.Conn Log logger.Logger TLS *tls.Config proto protocol.Protocol pingInterval time.Duration retrier util.AutoRedialer cookie string // status stateLock sync.RWMutex state ClientSessionState // authorization auth string // autoredial knobs shouldDelayP *uint32 lastAutoRedial time.Time redialDelay func(*clientSession) time.Duration redialJitter func(time.Duration) time.Duration redialDelays []time.Duration redialDelaysIdx int // connection events, and cookie reset requests, come in over here cmdCh chan sessCmd // last seen connection event is here lastConn bool // connection events are handled by this connHandler func(bool) // autoredial goes over here (xxx spurious goroutine involved) doneCh chan uint32 // main loop errors out through here (possibly another spurious goroutine) errCh chan error // main loop errors are handled by this errHandler func(error) // look, a stopper! stopCh chan struct{} } func redialDelay(sess *clientSession) time.Duration { if sess.ShouldDelay() { t := sess.redialDelays[sess.redialDelaysIdx] if len(sess.redialDelays) > sess.redialDelaysIdx+1 { sess.redialDelaysIdx++ } return t + sess.redialJitter(t) } else { sess.redialDelaysIdx = 0 return 0 } } func NewSession(serverAddrSpec string, conf ClientSessionConfig, deviceId string, seenStateFactory func() (seenstate.SeenState, error), log logger.Logger) (*clientSession, error) { seenState, err := seenStateFactory() if err != nil { return nil, err } var getHost hostGetter log.Infof("using addr: %v", serverAddrSpec) hostsEndpoint, fallbackHosts := parseServerAddrSpec(serverAddrSpec) if hostsEndpoint != "" { getHost = gethosts.New(deviceId, hostsEndpoint, conf.ExchangeTimeout) } var shouldDelay uint32 = 0 sess := &clientSession{ ClientSessionConfig: conf, getHost: getHost, fallbackHosts: fallbackHosts, DeviceId: deviceId, Log: log, Protocolator: protocol.NewProtocol0, SeenState: seenState, TLS: &tls.Config{}, state: Pristine, timeSince: time.Since, shouldDelayP: &shouldDelay, redialDelay: redialDelay, // NOTE there are tests that use calling sess.redialDelay as an indication of calling autoRedial! redialDelays: util.Timeouts(), } sess.redialJitter = sess.Jitter if sess.PEM != nil { cp := x509.NewCertPool() ok := cp.AppendCertsFromPEM(sess.PEM) if !ok { return nil, errors.New("could not parse certificate") } sess.TLS.RootCAs = cp block, _ := pem.Decode(sess.PEM) if block == nil { panic(fmt.Errorf("unexpected error reparsing certificate")) } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { panic(fmt.Errorf("unexpected error reparsing certificate: %v", err)) } // good guess var serverName string if len(cert.DNSNames) > 0 { serverName = cert.DNSNames[0] } else { serverName = cert.Subject.CommonName } sess.TLS.ServerName = serverName } sess.doneCh = make(chan uint32, 1) sess.stopCh = make(chan struct{}) sess.cmdCh = make(chan sessCmd) sess.errCh = make(chan error, 1) // to be overridden by tests sess.connHandler = sess.handleConn sess.errHandler = sess.handleErr return sess, nil } func (sess *clientSession) ShouldDelay() bool { return atomic.LoadUint32(sess.shouldDelayP) != 0 } func (sess *clientSession) setShouldDelay() { atomic.StoreUint32(sess.shouldDelayP, uint32(1)) } func (sess *clientSession) clearShouldDelay() { atomic.StoreUint32(sess.shouldDelayP, uint32(0)) } func (sess *clientSession) State() ClientSessionState { sess.stateLock.RLock() defer sess.stateLock.RUnlock() return sess.state } func (sess *clientSession) setState(state ClientSessionState) { sess.stateLock.Lock() defer sess.stateLock.Unlock() sess.Log.Debugf("session.setState: %s -> %s", sess.state, state) sess.state = state } func (sess *clientSession) setConnection(conn net.Conn) { sess.connLock.Lock() defer sess.connLock.Unlock() sess.Connection = conn } func (sess *clientSession) getConnection() net.Conn { sess.connLock.RLock() defer sess.connLock.RUnlock() return sess.Connection } func (sess *clientSession) setCookie(cookie string) { sess.connLock.Lock() defer sess.connLock.Unlock() sess.cookie = cookie } func (sess *clientSession) getCookie() string { sess.connLock.RLock() defer sess.connLock.RUnlock() return sess.cookie } func (sess *clientSession) ResetCookie() { sess.cmdCh <- cmdResetCookie } func (sess *clientSession) resetCookie() { sess.stopRedial() sess.doClose(true) } // getHosts sets deliveryHosts possibly querying a remote endpoint func (sess *clientSession) getHosts() error { if sess.getHost != nil { if sess.deliveryHosts != nil && sess.timeSince(sess.deliveryHostsTimestamp) < sess.HostsCachingExpiryTime { return nil } host, err := sess.getHost.Get() if err != nil { sess.Log.Errorf("getHosts: %v", err) sess.setState(Error) return err } sess.deliveryHostsTimestamp = time.Now() sess.deliveryHosts = host.Hosts if sess.TLS != nil { sess.TLS.ServerName = host.Domain } } else { sess.deliveryHosts = sess.fallbackHosts } return nil } // addAuthorization gets the authorization blob to send to the server // and adds it to the session. func (sess *clientSession) addAuthorization() error { if sess.AuthGetter != nil { sess.Log.Debugf("adding authorization") sess.auth = sess.AuthGetter(sess.AuthURL) } return nil } func (sess *clientSession) resetHosts() { sess.deliveryHosts = nil } // startConnectionAttempt/nextHostToTry help connect iterating over candidate hosts func (sess *clientSession) startConnectionAttempt() { if sess.timeSince(sess.lastAttemptTimestamp) > sess.ExpectAllRepairedTime { sess.tryHost = 0 } sess.leftToTry = len(sess.deliveryHosts) if sess.leftToTry == 0 { panic("should have got hosts from config or remote at this point") } sess.lastAttemptTimestamp = time.Now() } func (sess *clientSession) nextHostToTry() string { if sess.leftToTry == 0 { return "" } res := sess.deliveryHosts[sess.tryHost] sess.tryHost = (sess.tryHost + 1) % len(sess.deliveryHosts) sess.leftToTry-- return res } // we reached the Started state, we can retry with the same host if we // have to retry again func (sess *clientSession) started() { sess.tryHost-- if sess.tryHost == -1 { sess.tryHost = len(sess.deliveryHosts) - 1 } sess.setState(Started) } // connect to a server using the configuration in the ClientSession // and set up the connection. func (sess *clientSession) connect() error { sess.setShouldDelay() sess.startConnectionAttempt() var err error var conn net.Conn for { host := sess.nextHostToTry() if host == "" { sess.setState(Error) return fmt.Errorf("connect: %s", err) } sess.Log.Debugf("trying to connect to: %v", host) conn, err = net.DialTimeout("tcp", host, sess.ConnectTimeout) if err == nil { break } } sess.setConnection(tls.Client(conn, sess.TLS)) sess.setState(Connected) return nil } func (sess *clientSession) stopRedial() { if sess.retrier != nil { sess.retrier.Stop() sess.retrier = nil } } func (sess *clientSession) autoRedial() { sess.stopRedial() if time.Since(sess.lastAutoRedial) < 2*time.Second { sess.setShouldDelay() } // xxx should we really wait on the caller goroutine? delay := sess.redialDelay(sess) sess.Log.Debugf("session redial delay: %v, wait", delay) time.Sleep(delay) sess.Log.Debugf("session redial delay: %v, cont", delay) if sess.retrier != nil { panic("session AutoRedial: unexpected non-nil retrier.") } sess.retrier = util.NewAutoRedialer(sess) sess.lastAutoRedial = time.Now() go func(retrier util.AutoRedialer) { sess.Log.Debugf("session autoredialier launching Redial goroutine") // if the redialer has been stopped before calling Redial(), it'll return 0. sess.doneCh <- retrier.Redial() }(sess.retrier) } func (sess *clientSession) doClose(resetCookie bool) { sess.connLock.Lock() defer sess.connLock.Unlock() if resetCookie { sess.cookie = "" } sess.closeConnection() sess.setState(Disconnected) } func (sess *clientSession) closeConnection() { // *must be called with connLock held* if sess.Connection != nil { sess.Connection.Close() // we ignore Close errors, on purpose (the thinking being that // the connection isn't really usable, and you've got nothing // you could do to recover at this stage). sess.Connection = nil } } // handle "ping" messages func (sess *clientSession) handlePing() error { err := sess.proto.WriteMessage(protocol.PingPongMsg{Type: "pong"}) if err == nil { sess.Log.Debugf("ping.") sess.clearShouldDelay() } else { sess.setState(Error) sess.Log.Errorf("unable to pong: %s", err) } return err } func (sess *clientSession) decodeBroadcast(bcast *serverMsg) *BroadcastNotification { decoded := make([]map[string]interface{}, 0) for _, p := range bcast.Payloads { var v map[string]interface{} err := json.Unmarshal(p, &v) if err != nil { sess.Log.Debugf("expected map in broadcast: %v", err) continue } decoded = append(decoded, v) } return &BroadcastNotification{ TopLevel: bcast.TopLevel, Decoded: decoded, } } // handle "broadcast" messages func (sess *clientSession) handleBroadcast(bcast *serverMsg) error { err := sess.SeenState.SetLevel(bcast.ChanId, bcast.TopLevel) if err != nil { sess.setState(Error) sess.Log.Errorf("unable to set level: %v", err) sess.proto.WriteMessage(protocol.AckMsg{"nak"}) return err } // the server assumes if we ack the broadcast, we've updated // our levels. Hence the order. err = sess.proto.WriteMessage(protocol.AckMsg{"ack"}) if err != nil { sess.setState(Error) sess.Log.Errorf("unable to ack broadcast: %s", err) return err } sess.clearShouldDelay() sess.Log.Infof("broadcast chan:%v app:%v topLevel:%d payloads:%s", bcast.ChanId, bcast.AppId, bcast.TopLevel, bcast.Payloads) if bcast.ChanId == protocol.SystemChannelId { // the system channel id, the only one we care about for now sess.Log.Debugf("sending bcast over") sess.BroadcastCh <- sess.decodeBroadcast(bcast) sess.Log.Debugf("sent bcast over") } else { sess.Log.Errorf("what is this weird channel, %#v?", bcast.ChanId) } return nil } // handle "notifications" messages func (sess *clientSession) handleNotifications(ucast *serverMsg) error { notifs, err := sess.SeenState.FilterBySeen(ucast.Notifications) if err != nil { sess.setState(Error) sess.Log.Errorf("unable to record msgs seen: %v", err) sess.proto.WriteMessage(protocol.AckMsg{"nak"}) return err } // the server assumes if we ack the broadcast, we've updated // our state. Hence the order. err = sess.proto.WriteMessage(protocol.AckMsg{"ack"}) if err != nil { sess.setState(Error) sess.Log.Errorf("unable to ack notifications: %s", err) return err } sess.clearShouldDelay() sess.AddresseeChecker.StartAddresseeBatch() for i := range notifs { notif := ¬ifs[i] to := sess.AddresseeChecker.CheckForAddressee(notif) if to == nil { continue } sess.Log.Infof("unicast app:%v msg:%s payload:%s", notif.AppId, notif.MsgId, notif.Payload) sess.Log.Debugf("sending ucast over") sess.NotificationsCh <- AddressedNotification{to, notif} sess.Log.Debugf("sent ucast over") } return nil } // handle "connbroken" messages func (sess *clientSession) handleConnBroken(connBroken *serverMsg) error { sess.setState(Error) reason := connBroken.Reason err := fmt.Errorf("server broke connection: %s", reason) sess.Log.Errorf("%s", err) switch reason { case protocol.BrokenHostMismatch: sess.resetHosts() } return err } // handle "setparams" messages func (sess *clientSession) handleSetParams(setParams *serverMsg) error { if setParams.SetCookie != "" { sess.setCookie(setParams.SetCookie) } return nil } // loop runs the session with the server, emits a stream of events. func (sess *clientSession) loop() error { var err error var recv serverMsg sess.setState(Running) for { deadAfter := sess.pingInterval + sess.ExchangeTimeout sess.proto.SetDeadline(time.Now().Add(deadAfter)) err = sess.proto.ReadMessage(&recv) if err != nil { sess.Log.Debugf("session aborting with error on read.") sess.setState(Error) return err } switch recv.Type { case "ping": err = sess.handlePing() case "broadcast": err = sess.handleBroadcast(&recv) case "notifications": err = sess.handleNotifications(&recv) case "connbroken": err = sess.handleConnBroken(&recv) case "setparams": err = sess.handleSetParams(&recv) case "warn": // XXX: current message "warn" should be "connwarn" fallthrough case "connwarn": sess.Log.Errorf("server sent warning: %s", recv.Reason) } if err != nil { sess.Log.Debugf("session aborting with error from handler.") return err } } } // Call this when you've connected and want to start looping. func (sess *clientSession) start() error { conn := sess.getConnection() err := conn.SetDeadline(time.Now().Add(sess.ExchangeTimeout)) if err != nil { sess.setState(Error) sess.Log.Errorf("unable to start: set deadline: %s", err) return err } _, err = conn.Write(wireVersionBytes) // The Writer docs: Write must return a non-nil error if it returns // n < len(p). So, no need to check number of bytes written, hooray. if err != nil { sess.setState(Error) sess.Log.Errorf("unable to start: write version: %s", err) return err } proto := sess.Protocolator(conn) proto.SetDeadline(time.Now().Add(sess.ExchangeTimeout)) levels, err := sess.SeenState.GetAllLevels() if err != nil { sess.setState(Error) sess.Log.Errorf("unable to start: get levels: %v", err) return err } err = proto.WriteMessage(protocol.ConnectMsg{ Type: "connect", DeviceId: sess.DeviceId, Authorization: sess.auth, Cookie: sess.getCookie(), Levels: levels, Info: sess.Info, }) if err != nil { sess.setState(Error) sess.Log.Errorf("unable to start: connect: %s", err) return err } var connAck protocol.ConnAckMsg err = proto.ReadMessage(&connAck) if err != nil { sess.setState(Error) sess.Log.Errorf("unable to start: connack: %s", err) return err } if connAck.Type != "connack" { sess.setState(Error) return fmt.Errorf("expecting CONNACK, got %#v", connAck.Type) } pingInterval, err := time.ParseDuration(connAck.Params.PingInterval) if err != nil { sess.setState(Error) sess.Log.Errorf("unable to start: parse ping interval: %s", err) return err } sess.proto = proto sess.pingInterval = pingInterval sess.Log.Debugf("connected %v.", conn.RemoteAddr()) sess.started() // deals with choosing which host to retry with as well return nil } // run calls connect, and if it works it calls start, and if it works // it runs loop in a goroutine, and ships its return value over ErrCh. func (sess *clientSession) run(closer func(bool), authChecker, hostGetter, connecter, starter, looper func() error) error { closer(false) if err := authChecker(); err != nil { return err } if err := hostGetter(); err != nil { return err } err := connecter() if err == nil { err = starter() if err == nil { go func() { sess.errCh <- looper() }() } } return err } // This Jitter returns a random time.Duration somewhere in [-spread, spread]. func (sess *clientSession) Jitter(spread time.Duration) time.Duration { if spread < 0 { panic("spread must be non-negative") } n := int64(spread) return time.Duration(rand.Int63n(2*n+1) - n) } // Dial takes the session from newly created (or newly disconnected) // to running the main loop. func (sess *clientSession) Dial() error { if sess.Protocolator == nil { // a missing protocolator means you've willfully overridden // it; returning an error here would prompt AutoRedial to just // keep on trying. panic("can't Dial() without a protocol constructor.") } return sess.run(sess.doClose, sess.addAuthorization, sess.getHosts, sess.connect, sess.start, sess.loop) } func (sess *clientSession) shutdown() { sess.Log.Infof("session shutting down.") sess.connLock.Lock() defer sess.connLock.Unlock() sess.stopRedial() sess.closeConnection() } func (sess *clientSession) doKeepConnection() { for { select { case cmd := <-sess.cmdCh: switch cmd { case cmdConnect: sess.connHandler(true) case cmdDisconnect: sess.connHandler(false) case cmdResetCookie: sess.resetCookie() } case <-sess.stopCh: sess.shutdown() return case n := <-sess.doneCh: // if n == 0, the redialer aborted. If you do // anything other than log it, keep that in mind. sess.Log.Debugf("connected after %d attempts.", n) case err := <-sess.errCh: sess.errHandler(err) } } } func (sess *clientSession) handleConn(hasConn bool) { sess.lastConn = hasConn // Note this does not depend on the current state! That's because Dial // starts with doClose, which gets you to Disconnected even if you're // connected, and you can call Close when Disconnected without it // losing its stuff. if hasConn { sess.autoRedial() } else { sess.stopRedial() sess.doClose(false) } } func (sess *clientSession) handleErr(err error) { sess.Log.Errorf("session error'ed out with %v", err) // State() == Error mostly defends interrupting an ongoing // autoRedial if we went quickly already through hasConn = // false => hasConn = true if sess.State() == Error && sess.lastConn { sess.autoRedial() } } func (sess *clientSession) KeepConnection() error { sess.stateLock.Lock() defer sess.stateLock.Unlock() if sess.state != Pristine { return errors.New("don't call KeepConnection() on a non-pristine session.") } sess.state = Disconnected go sess.doKeepConnection() return nil } func (sess *clientSession) StopKeepConnection() { sess.setState(Shutdown) close(sess.stopCh) } func (sess *clientSession) HasConnectivity(hasConn bool) { if hasConn { sess.cmdCh <- cmdConnect } else { sess.cmdCh <- cmdDisconnect } } func init() { rand.Seed(time.Now().Unix()) // good enough for us (we're not using it for crypto) } ubuntu-push-0.68+16.04.20160310.2/client/client_test.go0000644000015600001650000011265212670364270022603 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package client import ( "encoding/json" "errors" "flag" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" //"runtime" "testing" "time" "launchpad.net/go-dbus/v1" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/accounts" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/bus/networkmanager" "launchpad.net/ubuntu-push/bus/systemimage" testibus "launchpad.net/ubuntu-push/bus/testing" "launchpad.net/ubuntu-push/click" clickhelp "launchpad.net/ubuntu-push/click/testing" "launchpad.net/ubuntu-push/client/service" "launchpad.net/ubuntu-push/client/session" "launchpad.net/ubuntu-push/config" "launchpad.net/ubuntu-push/identifier" idtesting "launchpad.net/ubuntu-push/identifier/testing" "launchpad.net/ubuntu-push/launch_helper" "launchpad.net/ubuntu-push/poller" "launchpad.net/ubuntu-push/protocol" helpers "launchpad.net/ubuntu-push/testing" "launchpad.net/ubuntu-push/testing/condition" "launchpad.net/ubuntu-push/util" ) func TestClient(t *testing.T) { TestingT(t) } // takeNext takes a value from given channel with a 5s timeout func takeNextBool(ch <-chan bool) bool { select { case <-time.After(5 * time.Second): panic("channel stuck: too long waiting") case v := <-ch: return v } } type dumbCommon struct { startCount int stopCount int runningCount int running bool err error } func (d *dumbCommon) Start() error { d.startCount++ return d.err } func (d *dumbCommon) Stop() { d.stopCount++ } func (d *dumbCommon) IsRunning() bool { d.runningCount++ return d.running } type dumbPush struct { dumbCommon unregCount int unregArgs []string } func (d *dumbPush) Unregister(appId string) error { d.unregCount++ d.unregArgs = append(d.unregArgs, appId) return d.err } type postArgs struct { app *click.AppId nid string payload json.RawMessage } type dumbPostal struct { dumbCommon bcastCount int postCount int postArgs []postArgs } func (d *dumbPostal) Post(app *click.AppId, nid string, payload json.RawMessage) { d.postCount++ if app.Application == "ubuntu-system-settings" { d.bcastCount++ } d.postArgs = append(d.postArgs, postArgs{app, nid, payload}) } var _ PostalService = (*dumbPostal)(nil) var _ PushService = (*dumbPush)(nil) type clientSuite struct { timeouts []time.Duration configPath string leveldbPath string log *helpers.TestLogger } var _ = Suite(&clientSuite{}) const ( staticText = "something ipsum dolor something" staticHash = "6155f83b471583f47c99998a472a178f" ) func mkHandler(text string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.(http.Flusher).Flush() w.Write([]byte(text)) w.(http.Flusher).Flush() } } func (cs *clientSuite) SetUpSuite(c *C) { config.IgnoreParsedFlags = true // because configure() uses newIdentifier = func() (identifier.Id, error) { id := idtesting.Settable() id.Set("42") // must be hex of len 32 return id, nil } cs.timeouts = util.SwapTimeouts([]time.Duration{0}) cs.leveldbPath = "" } func (cs *clientSuite) TearDownSuite(c *C) { util.SwapTimeouts(cs.timeouts) cs.timeouts = nil newIdentifier = identifier.New } func (cs *clientSuite) writeTestConfig(overrides map[string]interface{}) { pem_file := helpers.SourceRelative("../server/acceptance/ssl/testing.cert") cfgMap := map[string]interface{}{ "fallback_vibration": &launch_helper.Vibration{Pattern: []uint32{1}}, "fallback_sound": "sounds/ubuntu/notifications/Blip.ogg", "connect_timeout": "7ms", "exchange_timeout": "10ms", "hosts_cache_expiry": "1h", "expect_all_repaired": "30m", "stabilizing_timeout": "0ms", "connectivity_check_url": "", "connectivity_check_md5": "", "addr": ":0", "cert_pem_file": pem_file, "recheck_timeout": "3h", "auth_helper": "", "session_url": "xyzzy://", "registration_url": "reg://", "log_level": "debug", "poll_interval": "5m", "poll_settle": "20ms", "poll_net_wait": "1m", "poll_polld_wait": "3m", "poll_done_wait": "5s", "poll_busy_wait": "0s", } for k, v := range overrides { cfgMap[k] = v } cfgBlob, err := json.Marshal(cfgMap) if err != nil { panic(err) } ioutil.WriteFile(cs.configPath, cfgBlob, 0600) } func (cs *clientSuite) SetUpTest(c *C) { cs.log = helpers.NewTestLogger(c, "debug") dir := c.MkDir() cs.configPath = filepath.Join(dir, "config") cs.writeTestConfig(nil) } func (cs *clientSuite) TearDownTest(c *C) { //helpers.DumpGoroutines() } type sqlientSuite struct{ clientSuite } func (s *sqlientSuite) SetUpSuite(c *C) { s.clientSuite.SetUpSuite(c) s.leveldbPath = ":memory:" } var _ = Suite(&sqlientSuite{}) /***************************************************************** configure tests ******************************************************************/ func (cs *clientSuite) TestConfigureWorks(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() c.Assert(err, IsNil) c.Assert(cli.config, NotNil) c.Check(cli.config.ExchangeTimeout.TimeDuration(), Equals, time.Duration(10*time.Millisecond)) } func (cs *clientSuite) TestConfigureWorksWithFlags(c *C) { flag.CommandLine = flag.NewFlagSet("client", flag.ContinueOnError) os.Args = []string{"client", "-addr", "foo:7777"} cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() c.Assert(err, IsNil) c.Assert(cli.config, NotNil) c.Check(cli.config.Addr, Equals, "foo:7777") } func (cs *clientSuite) TestConfigureSetsUpLog(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) c.Check(cli.log, IsNil) err := cli.configure() c.Assert(err, IsNil) c.Assert(cli.log, NotNil) } func (cs *clientSuite) TestConfigureSetsUpPEM(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) c.Check(cli.pem, IsNil) err := cli.configure() c.Assert(err, IsNil) c.Assert(cli.pem, NotNil) } func (cs *clientSuite) TestConfigureSetsUpIdder(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) c.Check(cli.idder, IsNil) err := cli.configure() c.Assert(err, IsNil) c.Assert(cli.idder, NotNil) } func (cs *clientSuite) TestConfigureSetsUpEndpoints(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) c.Check(cli.connectivityEndp, IsNil) c.Check(cli.systemImageEndp, IsNil) err := cli.configure() c.Assert(err, IsNil) c.Check(cli.connectivityEndp, NotNil) c.Check(cli.systemImageEndp, NotNil) } func (cs *clientSuite) TestConfigureSetsUpConnCh(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) c.Check(cli.connCh, IsNil) err := cli.configure() c.Assert(err, IsNil) c.Assert(cli.connCh, NotNil) } func (cs *clientSuite) TestConfigureSetsUpAddresseeChecks(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) c.Check(cli.unregisterCh, IsNil) err := cli.configure() c.Assert(err, IsNil) c.Assert(cli.unregisterCh, NotNil) app, err := click.ParseAppId("com.bar.baz_foo") c.Assert(err, IsNil) c.Assert(cli.installedChecker.Installed(app, false), Equals, false) } func (cs *clientSuite) TestConfigureBailsOnBadFilename(c *C) { cli := NewPushClient("/does/not/exist", cs.leveldbPath) err := cli.configure() c.Assert(err, NotNil) } func (cs *clientSuite) TestConfigureBailsOnBadConfig(c *C) { cli := NewPushClient("/etc/passwd", cs.leveldbPath) err := cli.configure() c.Assert(err, NotNil) } func (cs *clientSuite) TestConfigureBailsOnBadPEMFilename(c *C) { cs.writeTestConfig(map[string]interface{}{ "cert_pem_file": "/a/b/c", }) cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() c.Assert(err, ErrorMatches, "reading PEM file: .*") } func (cs *clientSuite) TestConfigureBailsOnBadPEM(c *C) { cs.writeTestConfig(map[string]interface{}{ "cert_pem_file": "/etc/passwd", }) cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() c.Assert(err, ErrorMatches, "no PEM found.*") } func (cs *clientSuite) TestConfigureBailsOnNoHosts(c *C) { cs.writeTestConfig(map[string]interface{}{ "addr": " ", }) cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() c.Assert(err, ErrorMatches, "no hosts specified") } func (cs *clientSuite) TestConfigureRemovesBlanksInAddr(c *C) { cs.writeTestConfig(map[string]interface{}{ "addr": " foo: 443", }) cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() c.Assert(err, IsNil) c.Check(cli.config.Addr, Equals, "foo:443") } /***************************************************************** addresses checking tests ******************************************************************/ type testInstalledChecker func(*click.AppId, bool) bool func (tic testInstalledChecker) Installed(app *click.AppId, setVersion bool) bool { return tic(app, setVersion) } var ( appId1 = "com.example.app1_app1" appId2 = "com.example.app2_app2" appIdHello = "com.example.test_hello" app1 = clickhelp.MustParseAppId(appId1) app2 = clickhelp.MustParseAppId(appId2) appHello = clickhelp.MustParseAppId(appIdHello) ) func (cs *clientSuite) TestCheckForAddressee(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.unregisterCh = make(chan *click.AppId, 5) cli.StartAddresseeBatch() calls := 0 cli.installedChecker = testInstalledChecker(func(app *click.AppId, setVersion bool) bool { calls++ c.Assert(setVersion, Equals, true) if app.Original() == appId1 { return false } return true }) c.Check(cli.CheckForAddressee(&protocol.Notification{AppId: "bad-id"}), IsNil) c.Check(calls, Equals, 0) c.Assert(cli.unregisterCh, HasLen, 0) c.Check(cs.log.Captured(), Matches, `DEBUG notification "" for invalid app id "bad-id".\n`) cs.log.ResetCapture() c.Check(cli.CheckForAddressee(&protocol.Notification{AppId: appId1}), IsNil) c.Check(calls, Equals, 1) c.Assert(cli.unregisterCh, HasLen, 1) c.Check(<-cli.unregisterCh, DeepEquals, app1) c.Check(cs.log.Captured(), Matches, `DEBUG notification "" for missing app id "com.example.app1_app1".\n`) cs.log.ResetCapture() c.Check(cli.CheckForAddressee(&protocol.Notification{AppId: appId2}), DeepEquals, app2) c.Check(calls, Equals, 2) c.Check(cs.log.Captured(), Matches, "") cs.log.ResetCapture() c.Check(cli.CheckForAddressee(&protocol.Notification{AppId: appId1}), IsNil) c.Check(calls, Equals, 2) c.Check(cli.CheckForAddressee(&protocol.Notification{AppId: appId2}), DeepEquals, app2) c.Check(calls, Equals, 2) c.Check(cli.unregisterCh, HasLen, 0) c.Check(cs.log.Captured(), Matches, "") } /***************************************************************** deriveSessionConfig tests ******************************************************************/ func (cs *clientSuite) TestDeriveSessionConfig(c *C) { cs.writeTestConfig(map[string]interface{}{ "auth_helper": "auth helper", }) info := map[string]interface{}{ "foo": 1, } cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() c.Assert(err, IsNil) expected := session.ClientSessionConfig{ ConnectTimeout: 7 * time.Millisecond, ExchangeTimeout: 10 * time.Millisecond, HostsCachingExpiryTime: 1 * time.Hour, ExpectAllRepairedTime: 30 * time.Minute, PEM: cli.pem, Info: info, AuthGetter: func(string) string { return "" }, AuthURL: "xyzzy://", AddresseeChecker: cli, BroadcastCh: make(chan *session.BroadcastNotification), NotificationsCh: make(chan session.AddressedNotification), } // sanity check that we are looking at all fields vExpected := reflect.ValueOf(expected) nf := vExpected.NumField() for i := 0; i < nf; i++ { fv := vExpected.Field(i) // field isn't empty/zero c.Assert(fv.Interface(), Not(DeepEquals), reflect.Zero(fv.Type()).Interface(), Commentf("forgot about: %s", vExpected.Type().Field(i).Name)) } // finally compare conf := cli.deriveSessionConfig(info) // compare authGetter by string c.Check(fmt.Sprintf("%#v", conf.AuthGetter), Equals, fmt.Sprintf("%#v", cli.getAuthorization)) // channels are ok as long as non-nil conf.BroadcastCh = nil conf.NotificationsCh = nil expected.BroadcastCh = nil expected.NotificationsCh = nil // and set it to nil conf.AuthGetter = nil expected.AuthGetter = nil c.Check(conf, DeepEquals, expected) } /***************************************************************** derivePushServiceSetup tests ******************************************************************/ func (cs *clientSuite) TestDerivePushServiceSetup(c *C) { cs.writeTestConfig(map[string]interface{}{}) cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() c.Assert(err, IsNil) cli.deviceId = "zoo" expected := &service.PushServiceSetup{ DeviceId: "zoo", AuthGetter: func(string) string { return "" }, RegURL: helpers.ParseURL("reg://"), InstalledChecker: cli.installedChecker, } // sanity check that we are looking at all fields vExpected := reflect.ValueOf(expected).Elem() nf := vExpected.NumField() for i := 0; i < nf; i++ { fv := vExpected.Field(i) // field isn't empty/zero c.Assert(fv.Interface(), Not(DeepEquals), reflect.Zero(fv.Type()).Interface(), Commentf("forgot about: %s", vExpected.Type().Field(i).Name)) } // finally compare setup, err := cli.derivePushServiceSetup() c.Assert(err, IsNil) // compare authGetter by string c.Check(fmt.Sprintf("%#v", setup.AuthGetter), Equals, fmt.Sprintf("%#v", cli.getAuthorization)) // and set it to nil setup.AuthGetter = nil expected.AuthGetter = nil c.Check(setup, DeepEquals, expected) } func (cs *clientSuite) TestDerivePushServiceSetupError(c *C) { cs.writeTestConfig(map[string]interface{}{ "registration_url": "%gh", }) cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() c.Assert(err, IsNil) _, err = cli.derivePushServiceSetup() c.Check(err, ErrorMatches, "cannot parse registration url:.*") } /***************************************************************** derivePostalConfig tests ******************************************************************/ func (cs *clientSuite) TestDerivePostalServiceSetup(c *C) { cs.writeTestConfig(map[string]interface{}{}) cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() c.Assert(err, IsNil) expected := &service.PostalServiceSetup{ InstalledChecker: cli.installedChecker, FallbackVibration: cli.config.FallbackVibration, FallbackSound: cli.config.FallbackSound, } // sanity check that we are looking at all fields vExpected := reflect.ValueOf(expected).Elem() nf := vExpected.NumField() for i := 0; i < nf; i++ { fv := vExpected.Field(i) // field isn't empty/zero c.Assert(fv.Interface(), Not(DeepEquals), reflect.Zero(fv.Type()).Interface(), Commentf("forgot about: %s", vExpected.Type().Field(i).Name)) } // finally compare setup := cli.derivePostalServiceSetup() c.Check(setup, DeepEquals, expected) } /***************************************************************** derivePollerSetup tests ******************************************************************/ type derivePollerSession struct{} func (s *derivePollerSession) ResetCookie() {} func (s *derivePollerSession) State() session.ClientSessionState { return session.Unknown } func (s *derivePollerSession) HasConnectivity(bool) {} func (s *derivePollerSession) KeepConnection() error { return nil } func (s *derivePollerSession) StopKeepConnection() {} func (cs *clientSuite) TestDerivePollerSetup(c *C) { cs.writeTestConfig(map[string]interface{}{}) cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.session = new(derivePollerSession) err := cli.configure() c.Assert(err, IsNil) expected := &poller.PollerSetup{ Times: poller.Times{ AlarmInterval: 5 * time.Minute, SessionStateSettle: 20 * time.Millisecond, NetworkWait: time.Minute, PolldWait: 3 * time.Minute, DoneWait: 5 * time.Second, }, Log: cli.log, SessionStateGetter: cli.session, } // sanity check that we are looking at all fields vExpected := reflect.ValueOf(expected).Elem() nf := vExpected.NumField() for i := 0; i < nf; i++ { fv := vExpected.Field(i) // field isn't empty/zero c.Assert(fv.Interface(), Not(DeepEquals), reflect.Zero(fv.Type()).Interface(), Commentf("forgot about: %s", vExpected.Type().Field(i).Name)) } // finally compare setup := cli.derivePollerSetup() c.Check(setup, DeepEquals, expected) } /***************************************************************** startService tests ******************************************************************/ func (cs *clientSuite) TestStartPushServiceCallsStart(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) d := new(dumbPush) cli.pushService = d c.Check(cli.startPushService(), IsNil) c.Check(d.startCount, Equals, 1) } func (cs *clientSuite) TestStartPostServiceCallsStart(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) d := new(dumbPostal) cli.postalService = d c.Check(cli.startPostalService(), IsNil) c.Check(d.startCount, Equals, 1) } func (cs *clientSuite) TestSetupPushServiceSetupError(c *C) { cs.writeTestConfig(map[string]interface{}{ "registration_url": "%gh", }) cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() c.Assert(err, IsNil) err = cli.setupPushService() c.Check(err, ErrorMatches, "cannot parse registration url:.*") } func (cs *clientSuite) TestSetupPushService(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) c.Assert(cli.configure(), IsNil) c.Check(cli.pushService, IsNil) c.Check(cli.setupPushService(), IsNil) c.Check(cli.pushService, NotNil) } func (cs *clientSuite) TestStartPushErrorsOnPushStartError(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) d := new(dumbPush) err := errors.New("potato") d.err = err cli.pushService = d c.Check(cli.startPushService(), Equals, err) c.Check(d.startCount, Equals, 1) } func (cs *clientSuite) TestStartPostalErrorsOnPostalStartError(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) d := new(dumbPostal) err := errors.New("potato") d.err = err cli.postalService = d c.Check(cli.startPostalService(), Equals, err) c.Check(d.startCount, Equals, 1) } /***************************************************************** getDeviceId tests ******************************************************************/ func (cs *clientSuite) TestGetDeviceIdWorks(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.idder, _ = identifier.New() c.Check(cli.deviceId, Equals, "") c.Check(cli.getDeviceId(), IsNil) c.Check(cli.deviceId, HasLen, 40) } func (cs *clientSuite) TestGetDeviceIdCanFail(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.idder = idtesting.Failing() c.Check(cli.deviceId, Equals, "") c.Check(cli.getDeviceId(), NotNil) } func (cs *clientSuite) TestGetDeviceIdIdentifierDoesTheUnexpected(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log settable := idtesting.Settable() cli.idder = settable settable.Set("not-hex") c.Check(cli.deviceId, Equals, "") c.Check(cli.getDeviceId(), ErrorMatches, "machine-id should be hex: .*") } /***************************************************************** takeTheBus tests ******************************************************************/ func (cs *clientSuite) TestTakeTheBusWorks(c *C) { // http server used for connectivity test ts := httptest.NewServer(mkHandler(staticText)) defer ts.Close() // testing endpoints cCond := condition.Fail2Work(7) cEndp := testibus.NewTestingEndpoint(cCond, condition.Work(true), uint32(networkmanager.Connecting), dbus.ObjectPath("hello"), uint32(networkmanager.Connecting), dbus.ObjectPath("hello"), ) siCond := condition.Fail2Work(2) siEndp := testibus.NewMultiValuedTestingEndpoint(siCond, condition.Work(true), []interface{}{map[string]string{ "version_detail": "ubuntu=20160304.2,device=20160304.2,custom=20160304.2,version=381", "last_update_date": "2016-03-04 15:25:31", "last_check_date": "2016-03-08 04:30:34", "target_version_detail": "-1", "device_name": "mako", "target_build_number": "-1", "channel_name": "ubuntu-touch/rc-proposed/ubuntu", "current_build_number": "381", }}) tickerCh := make(chan []interface{}) nopTickerCh := make(chan []interface{}) testibus.SetWatchSource(cEndp, "StateChanged", tickerCh) testibus.SetWatchSource(cEndp, "PropertiesChanged", nopTickerCh) defer close(tickerCh) defer close(nopTickerCh) // ok, create the thing cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log err := cli.configure() c.Assert(err, IsNil) // and stomp on things for testing cli.config.ConnectivityConfig.ConnectivityCheckURL = ts.URL cli.config.ConnectivityConfig.ConnectivityCheckMD5 = staticHash cli.connectivityEndp = cEndp cli.systemImageEndp = siEndp c.Assert(cli.takeTheBus(), IsNil) c.Check(takeNextBool(cli.connCh), Equals, false) tickerCh <- []interface{}{uint32(networkmanager.ConnectedGlobal)} c.Check(takeNextBool(cli.connCh), Equals, true) // the connectivity endpoint retried until connected c.Check(cCond.OK(), Equals, true) // the systemimage endpoint retried until connected c.Check(siCond.OK(), Equals, true) } // takeTheBus can, in fact, fail func (cs *clientSuite) TestTakeTheBusCanFail(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) err := cli.configure() cli.log = cs.log c.Assert(err, IsNil) // and stomp on things for testing cli.connectivityEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false)) cli.systemImageEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(false)) c.Check(cli.takeTheBus(), NotNil) c.Assert(cli.setupPostalService(), IsNil) } /***************************************************************** seenStateFactory tests ******************************************************************/ func (cs *clientSuite) TestSeenStateFactoryNoDbPath(c *C) { cli := NewPushClient(cs.configPath, "") ln, err := cli.seenStateFactory() c.Assert(err, IsNil) defer ln.Close() c.Check(fmt.Sprintf("%T", ln), Equals, "*seenstate.memSeenState") } func (cs *clientSuite) TestSeenStateFactoryWithDbPath(c *C) { cli := NewPushClient(cs.configPath, ":memory:") ln, err := cli.seenStateFactory() c.Assert(err, IsNil) defer ln.Close() c.Check(fmt.Sprintf("%T", ln), Equals, "*seenstate.sqliteSeenState") } /***************************************************************** filterBroadcastNotification tests ******************************************************************/ var siInfoRes = &systemimage.InfoResult{ Device: "mako", Channel: "daily", BuildNumber: 102, LastUpdate: "Unknown", } func (cs *clientSuite) TestFilterBroadcastNotification(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.systemImageInfo = siInfoRes // empty msg := &session.BroadcastNotification{} c.Check(cli.filterBroadcastNotification(msg), Equals, false) // same build number, we let the helper deal msg = &session.BroadcastNotification{ Decoded: []map[string]interface{}{ map[string]interface{}{ "daily/mako": []interface{}{float64(102), "tubular"}, }, }, } c.Check(cli.filterBroadcastNotification(msg), Equals, true) } func (cs *clientSuite) TestFilterBroadcastNotificationRobust(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.systemImageInfo = siInfoRes msg := &session.BroadcastNotification{ Decoded: []map[string]interface{}{ map[string]interface{}{}, }, } c.Check(cli.filterBroadcastNotification(msg), Equals, false) for _, broken := range []interface{}{ 5, []interface{}{}, []interface{}{55}, } { msg := &session.BroadcastNotification{ Decoded: []map[string]interface{}{ map[string]interface{}{ "daily/mako": broken, }, }, } c.Check(cli.filterBroadcastNotification(msg), Equals, false) } } /***************************************************************** handleBroadcastNotification tests ******************************************************************/ var ( positiveBroadcastNotification = &session.BroadcastNotification{ Decoded: []map[string]interface{}{ map[string]interface{}{ "daily/mako": []interface{}{float64(102), "tubular"}, }, map[string]interface{}{ "daily/mako": []interface{}{float64(103), "tubular"}, }, }, } negativeBroadcastNotification = &session.BroadcastNotification{ Decoded: []map[string]interface{}{ map[string]interface{}{}, }, } ) func (cs *clientSuite) TestHandleBroadcastNotification(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.systemImageInfo = siInfoRes cli.log = cs.log d := new(dumbPostal) cli.postalService = d c.Check(cli.handleBroadcastNotification(positiveBroadcastNotification), IsNil) // we dun posted c.Check(d.bcastCount, Equals, 1) c.Assert(d.postArgs, HasLen, 1) expectedApp, _ := click.ParseAppId("_ubuntu-system-settings") c.Check(d.postArgs[0].app, DeepEquals, expectedApp) c.Check(d.postArgs[0].nid, Equals, "") expectedData, _ := json.Marshal(positiveBroadcastNotification.Decoded[1]) c.Check([]byte(d.postArgs[0].payload), DeepEquals, expectedData) } func (cs *clientSuite) TestHandleBroadcastNotificationNothingToDo(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.systemImageInfo = siInfoRes cli.log = cs.log d := new(dumbPostal) cli.postalService = d c.Check(cli.handleBroadcastNotification(negativeBroadcastNotification), IsNil) // we not dun no posted c.Check(d.bcastCount, Equals, 0) } /***************************************************************** handleUnicastNotification tests ******************************************************************/ var payload = `{"message": "aGVsbG8=", "notification": {"card": {"icon": "icon-value", "summary": "summary-value", "body": "body-value", "actions": []}}}` var notif = &protocol.Notification{AppId: appIdHello, Payload: []byte(payload), MsgId: "42"} func (cs *clientSuite) TestHandleUcastNotification(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log d := new(dumbPostal) cli.postalService = d c.Check(cli.handleUnicastNotification(session.AddressedNotification{appHello, notif}), IsNil) // check we sent the notification c.Check(d.postCount, Equals, 1) c.Assert(d.postArgs, HasLen, 1) c.Check(d.postArgs[0].app, Equals, appHello) c.Check(d.postArgs[0].nid, Equals, notif.MsgId) c.Check(d.postArgs[0].payload, DeepEquals, notif.Payload) } /***************************************************************** handleUnregister tests ******************************************************************/ type testPushService struct { err error unregistered string } func (ps *testPushService) Start() error { return nil } func (ps *testPushService) Unregister(appId string) error { ps.unregistered = appId return ps.err } func (cs *clientSuite) TestHandleUnregister(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.installedChecker = testInstalledChecker(func(app *click.AppId, setVersion bool) bool { c.Check(setVersion, Equals, false) c.Check(app.Original(), Equals, appId1) return false }) ps := &testPushService{} cli.pushService = ps cli.handleUnregister(app1) c.Assert(ps.unregistered, Equals, appId1) c.Check(cs.log.Captured(), Equals, "DEBUG unregistered token for com.example.app1_app1\n") } func (cs *clientSuite) TestHandleUnregisterNop(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.installedChecker = testInstalledChecker(func(app *click.AppId, setVersion bool) bool { c.Check(setVersion, Equals, false) c.Check(app.Original(), Equals, appId1) return true }) ps := &testPushService{} cli.pushService = ps cli.handleUnregister(app1) c.Assert(ps.unregistered, Equals, "") } func (cs *clientSuite) TestHandleUnregisterError(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.installedChecker = testInstalledChecker(func(app *click.AppId, setVersion bool) bool { return false }) fail := errors.New("BAD") ps := &testPushService{err: fail} cli.pushService = ps cli.handleUnregister(app1) c.Check(cs.log.Captured(), Matches, "ERROR unregistering com.example.app1_app1: BAD\n") } /***************************************************************** doLoop tests ******************************************************************/ var nopConn = func(bool) {} var nopBcast = func(*session.BroadcastNotification) error { return nil } var nopUcast = func(session.AddressedNotification) error { return nil } var nopUnregister = func(*click.AppId) {} var nopAcct = func() {} func (cs *clientSuite) TestDoLoopConn(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.systemImageInfo = siInfoRes cli.connCh = make(chan bool, 1) cli.connCh <- true c.Assert(cli.initSessionAndPoller(), IsNil) ch := make(chan bool, 1) go cli.doLoop(func(bool) { ch <- true }, nopBcast, nopUcast, nopUnregister, nopAcct) c.Check(takeNextBool(ch), Equals, true) } func (cs *clientSuite) TestDoLoopBroadcast(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.systemImageInfo = siInfoRes c.Assert(cli.initSessionAndPoller(), IsNil) cli.broadcastCh = make(chan *session.BroadcastNotification, 1) cli.broadcastCh <- &session.BroadcastNotification{} ch := make(chan bool, 1) go cli.doLoop(nopConn, func(_ *session.BroadcastNotification) error { ch <- true; return nil }, nopUcast, nopUnregister, nopAcct) c.Check(takeNextBool(ch), Equals, true) } func (cs *clientSuite) TestDoLoopNotif(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.systemImageInfo = siInfoRes c.Assert(cli.initSessionAndPoller(), IsNil) cli.notificationsCh = make(chan session.AddressedNotification, 1) cli.notificationsCh <- session.AddressedNotification{} ch := make(chan bool, 1) go cli.doLoop(nopConn, nopBcast, func(session.AddressedNotification) error { ch <- true; return nil }, nopUnregister, nopAcct) c.Check(takeNextBool(ch), Equals, true) } func (cs *clientSuite) TestDoLoopUnregister(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.systemImageInfo = siInfoRes c.Assert(cli.initSessionAndPoller(), IsNil) cli.unregisterCh = make(chan *click.AppId, 1) cli.unregisterCh <- app1 ch := make(chan bool, 1) go cli.doLoop(nopConn, nopBcast, nopUcast, func(app *click.AppId) { c.Check(app.Original(), Equals, appId1); ch <- true }, nopAcct) c.Check(takeNextBool(ch), Equals, true) } func (cs *clientSuite) TestDoLoopAcct(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.systemImageInfo = siInfoRes c.Assert(cli.initSessionAndPoller(), IsNil) acctCh := make(chan accounts.Changed, 1) acctCh <- accounts.Changed{} cli.accountsCh = acctCh ch := make(chan bool, 1) go cli.doLoop(nopConn, nopBcast, nopUcast, nopUnregister, func() { ch <- true }) c.Check(takeNextBool(ch), Equals, true) } /***************************************************************** doStart tests ******************************************************************/ func (cs *clientSuite) TestDoStartWorks(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) one_called := false two_called := false one := func() error { one_called = true; return nil } two := func() error { two_called = true; return nil } c.Check(cli.doStart(one, two), IsNil) c.Check(one_called, Equals, true) c.Check(two_called, Equals, true) } func (cs *clientSuite) TestDoStartFailsAsExpected(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) one_called := false two_called := false failure := errors.New("Failure") one := func() error { one_called = true; return failure } two := func() error { two_called = true; return nil } c.Check(cli.doStart(one, two), Equals, failure) c.Check(one_called, Equals, true) c.Check(two_called, Equals, false) } /***************************************************************** Loop() tests ******************************************************************/ type loopSession struct{ hasConn bool } type loopPoller struct{} func (s *loopSession) ResetCookie() {} func (s *loopSession) State() session.ClientSessionState { if s.hasConn { return session.Connected } else { return session.Disconnected } } func (s *loopSession) HasConnectivity(hasConn bool) { s.hasConn = hasConn } func (s *loopSession) KeepConnection() error { return nil } func (s *loopSession) StopKeepConnection() {} func (p *loopPoller) HasConnectivity(hasConn bool) {} func (p *loopPoller) IsConnected() bool { return false } func (p *loopPoller) Start() error { return nil } func (p *loopPoller) Run() error { return nil } func (cs *clientSuite) TestLoop(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.connCh = make(chan bool) cli.sessionConnectedCh = make(chan uint32) cli.log = cs.log cli.connectivityEndp = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(networkmanager.ConnectedGlobal)) cli.systemImageInfo = siInfoRes d := new(dumbPostal) cli.postalService = d c.Assert(cli.startPostalService(), IsNil) c.Assert(cli.initSessionAndPoller(), IsNil) cli.broadcastCh = make(chan *session.BroadcastNotification) // we use tick() to make sure things have been through the // event loop at least once before looking at things; // otherwise there's a race between what we're trying to look // at and the loop itself. tick := func() { cli.sessionConnectedCh <- 42 } c.Assert(cli.session, NotNil) cli.session.StopKeepConnection() cli.session = &loopSession{} cli.poller = &loopPoller{} go cli.Loop() // sessionConnectedCh to nothing in particular, but it'll help sync this test cli.sessionConnectedCh <- 42 tick() c.Check(cs.log.Captured(), Matches, "(?msi).*Session connected after 42 attempts$") // loop() should have connected: // * connCh to the connectivity checker c.Check(cli.session.State(), Equals, session.Disconnected) cli.connCh <- true tick() c.Check(cli.session.State(), Equals, session.Connected) cli.connCh <- false tick() c.Check(cli.session.State(), Equals, session.Disconnected) // * session.BroadcastCh to the notifications handler c.Check(d.bcastCount, Equals, 0) cli.broadcastCh <- positiveBroadcastNotification tick() c.Check(d.bcastCount, Equals, 1) } /***************************************************************** Start() tests ******************************************************************/ // XXX this is a hack. func (cs *clientSuite) hasDbus() bool { for _, b := range []bus.Bus{bus.SystemBus, bus.SessionBus} { if b.Endpoint(bus.BusDaemonAddress, cs.log).Dial() != nil { return false } } return true } func (cs *clientSuite) TestStart(c *C) { if !cs.hasDbus() { c.Skip("no dbus") } cli := NewPushClient(cs.configPath, cs.leveldbPath) // before start, everything sucks: // no service, c.Check(cli.pushService, IsNil) // no config, c.Check(string(cli.config.Addr), Equals, "") // no device id, c.Check(cli.deviceId, HasLen, 0) // no session, c.Check(cli.session, IsNil) // no bus, c.Check(cli.systemImageEndp, IsNil) // no nuthin'. // so we start, err := cli.Start() // and it works c.Assert(err, IsNil) // and now everthing is better! We have a config, c.Check(string(cli.config.Addr), Equals, ":0") // and a device id, c.Check(cli.deviceId, HasLen, 40) // and a session, c.Check(cli.session, NotNil) // and a bus, c.Check(cli.systemImageEndp, NotNil) // and a service, c.Check(cli.pushService, NotNil) // and everthying us just peachy! cli.pushService.(*service.PushService).Stop() // cleanup cli.postalService.Stop() // cleanup } func (cs *clientSuite) TestStartCanFail(c *C) { cli := NewPushClient("/does/not/exist", cs.leveldbPath) // easiest way for it to fail is to feed it a bad config err := cli.Start() // and it works. Err. Doesn't. c.Check(err, NotNil) } func (cs *clientSuite) TestinitSessionAndPollerErr(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log cli.systemImageInfo = siInfoRes // change the cli.pem value so initSessionAndPoller fails cli.pem = []byte("foo") c.Assert(cli.initSessionAndPoller(), NotNil) } /***************************************************************** getAuthorization() tests ******************************************************************/ func (cs *clientSuite) TestGetAuthorizationIgnoresErrors(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.configure() cli.config.AuthHelper = "/no/such/executable" c.Check(cli.getAuthorization("xyzzy://"), Equals, "") } func (cs *clientSuite) TestGetAuthorizationGetsIt(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.configure() cli.config.AuthHelper = helpers.ScriptAbsPath("dummyauth.sh") c.Check(cli.getAuthorization("xyzzy://"), Equals, "hello xyzzy://") } func (cs *clientSuite) TestGetAuthorizationWorksIfUnsetOrNil(c *C) { cli := NewPushClient(cs.configPath, cs.leveldbPath) cli.log = cs.log c.Assert(cli.config, NotNil) c.Check(cli.getAuthorization("xyzzy://"), Equals, "") cli.configure() c.Check(cli.getAuthorization("xyzzy://"), Equals, "") } ubuntu-push-0.68+16.04.20160310.2/dependencies.tsv0000644000015600001650000000062112670364255021640 0ustar pbuserpbgroup00000000000000code.google.com/p/go-uuid hg 7dda39b2e7d5e265014674c5af696ba4186679e9 11 code.google.com/p/gosqlite hg 74691fb6f83716190870cde1b658538dd4b18eb0 15 launchpad.net/go-dbus/v1 bzr jlenton@gmail.com-20141023032446-s5icvsucwlv5o38a 129 launchpad.net/go-xdg/v0 bzr john.lenton@canonical.com-20140208094800-gubd5md7cro3mtxa 10 launchpad.net/gocheck bzr gustavo@niemeyer.net-20140225173054-xu9zlkf9kxhvow02 87 ubuntu-push-0.68+16.04.20160310.2/Makefile0000644000015600001650000000616112670364270020116 0ustar pbuserpbgroup00000000000000GOPATH := $(shell cd ../../..; pwd) export GOPATH PROJECT = launchpad.net/ubuntu-push ifneq ($(CURDIR),$(GOPATH)/src/launchpad.net/ubuntu-push) $(error unexpected curdir and/or layout) endif GODEPS = launchpad.net/gocheck GODEPS += launchpad.net/go-dbus/v1 GODEPS += launchpad.net/go-xdg/v0 GODEPS += code.google.com/p/gosqlite/sqlite3 GODEPS += code.google.com/p/go-uuid/uuid # cgocheck=0 is a workaround for lp:1555198 GOTEST := GODEBUG=cgocheck=0 ./scripts/goctest TOTEST = $(shell env GOPATH=$(GOPATH) go list $(PROJECT)/...|grep -v acceptance|grep -v http13client ) TOBUILD = $(shell grep -lr '^package main') all: fetchdeps bootstrap build-client build-server-dev fetchdeps: .has-fetched-deps .has-fetched-deps: PACKAGE_DEPS @$(MAKE) --no-print-directory refetchdeps @touch $@ refetchdeps: sudo apt-get install $$( grep -v '^#' PACKAGE_DEPS ) bootstrap: dependencies.tsv $(RM) -r $(GOPATH)/pkg mkdir -p $(GOPATH)/bin mkdir -p $(GOPATH)/pkg go get -u launchpad.net/godeps go get -d -u $(GODEPS) $(GOPATH)/bin/godeps -u dependencies.tsv go install $(GODEPS) dependencies.tsv: $(GOPATH)/bin/godeps -t $(TOTEST) $(foreach i,$(TOBUILD),$(dir $(PROJECT)/$(i))) 2>/dev/null | cat > $@ check: $(GOTEST) $(TESTFLAGS) $(TOTEST) check-race: $(GOTEST) $(TESTFLAGS) -race $(TOTEST) acceptance: cd server/acceptance; ./acceptance.sh build-client: ubuntu-push-client signing-helper/signing-helper %.deps: % $(SH) scripts/deps.sh $< %: %.go go build -o $@ $< include $(TOBUILD:.go=.go.deps) signing-helper/Makefile: signing-helper/CMakeLists.txt signing-helper/signing-helper.cpp signing-helper/signing.h cd signing-helper && (make clean || true) && cmake . signing-helper/signing-helper: signing-helper/Makefile signing-helper/signing-helper.cpp signing-helper/signing.h cd signing-helper && make build-server-dev: push-server-dev run-server-dev: push-server-dev ./$< sampleconfigs/dev.json push-server-dev: server/dev/server mv $< $@ # very basic cleanup stuff; needs more work clean: $(RM) -r coverhtml $(MAKE) -C signing-helper clean || true $(RM) push-server-dev $(RM) $(TOBUILD:.go=) distclean: bzr clean-tree --verbose --ignored --force coverage-summary: $(GOTEST) $(TESTFLAGS) -a -cover $(TOTEST) coverage-html: mkdir -p coverhtml for pkg in $(TOTEST); do \ relname="$${pkg#$(PROJECT)/}" ; \ mkdir -p coverhtml/$$(dirname $${relname}) ; \ $(GOTEST) $(TESTFLAGS) -a -coverprofile=coverhtml/$${relname}.out $$pkg ; \ if [ -f coverhtml/$${relname}.out ] ; then \ go tool cover -html=coverhtml/$${relname}.out -o coverhtml/$${relname}.html ; \ go tool cover -func=coverhtml/$${relname}.out -o coverhtml/$${relname}.txt ; \ fi \ done format: go fmt $(PROJECT)/... check-format: scripts/check_fmt $(PROJECT) protocol-diagrams: protocol/state-diag-client.svg protocol/state-diag-session.svg %.svg: %.gv # requires graphviz installed dot -Tsvg $< > $@ .PHONY: bootstrap check check-race format check-format \ acceptance build-client build-server-dev run-server-dev \ coverage-summary coverage-html protocol-diagrams \ fetchdeps refetchdeps clean distclean all .INTERMEDIATE: server/dev/server ubuntu-push-0.68+16.04.20160310.2/config/0000755000015600001650000000000012670364532017720 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/config/config_test.go0000644000015600001650000002422712670364255022564 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package config import ( "bytes" "encoding/json" "flag" "fmt" "io/ioutil" "os" "path/filepath" "reflect" "testing" "time" . "launchpad.net/gocheck" ) func TestConfig(t *testing.T) { TestingT(t) } type configSuite struct{} var _ = Suite(&configSuite{}) type testConfig1 struct { A int B string C []string `json:"c_list"` } func (s *configSuite) TestReadConfig(c *C) { buf := bytes.NewBufferString(`{"a": 1, "b": "foo", "c_list": ["c", "d", "e"]}`) var cfg testConfig1 err := ReadConfig(buf, &cfg) c.Check(err, IsNil) c.Check(cfg, DeepEquals, testConfig1{A: 1, B: "foo", C: []string{"c", "d", "e"}}) } func checkError(c *C, config string, dest interface{}, expectedError string) { buf := bytes.NewBufferString(config) err := ReadConfig(buf, dest) c.Check(err, ErrorMatches, expectedError) } func (s *configSuite) TestReadConfigErrors(c *C) { var cfg testConfig1 checkError(c, "", cfg, `destConfig not \*struct`) var i int checkError(c, "", &i, `destConfig not \*struct`) checkError(c, "", &cfg, `EOF`) checkError(c, `{"a": "1"}`, &cfg, `a: .*type int`) checkError(c, `{"b": "1"}`, &cfg, `missing a`) checkError(c, `{"A": "1"}`, &cfg, `missing a`) checkError(c, `{"a": 1, "b": "foo"}`, &cfg, `missing c_list`) } type testTimeDurationConfig struct { D ConfigTimeDuration } func (s *configSuite) TestReadConfigTimeDuration(c *C) { buf := bytes.NewBufferString(`{"d": "2s"}`) var cfg testTimeDurationConfig err := ReadConfig(buf, &cfg) c.Assert(err, IsNil) c.Check(cfg.D.TimeDuration(), Equals, 2*time.Second) } func (s *configSuite) TestReadConfigTimeDurationErrors(c *C) { var cfg testTimeDurationConfig checkError(c, `{"d": 1}`, &cfg, "d:.*type string") checkError(c, `{"d": "2"}`, &cfg, "d:.*missing unit.*") } type testHostPortConfig struct { H ConfigHostPort } func (s *configSuite) TestReadConfigHostPort(c *C) { buf := bytes.NewBufferString(`{"h": "127.0.0.1:9999"}`) var cfg testHostPortConfig err := ReadConfig(buf, &cfg) c.Assert(err, IsNil) c.Check(cfg.H.HostPort(), Equals, "127.0.0.1:9999") } func (s *configSuite) TestReadConfigHostPortErrors(c *C) { var cfg testHostPortConfig checkError(c, `{"h": 1}`, &cfg, "h:.*type string") checkError(c, `{"h": ""}`, &cfg, "h: missing port in address") } type testQueueSizeConfig struct { QS ConfigQueueSize } func (s *configSuite) TestReadConfigQueueSize(c *C) { buf := bytes.NewBufferString(`{"qS": 1}`) var cfg testQueueSizeConfig err := ReadConfig(buf, &cfg) c.Assert(err, IsNil) c.Check(cfg.QS.QueueSize(), Equals, uint(1)) } func (s *configSuite) TestReadConfigQueueSizeErrors(c *C) { var cfg testQueueSizeConfig checkError(c, `{"qS": "x"}`, &cfg, "qS: .*type uint") checkError(c, `{"qS": 0}`, &cfg, "qS: queue size should be > 0") } func (s *configSuite) TestLoadFile(c *C) { tmpDir := c.MkDir() d, err := LoadFile("", tmpDir) c.Check(err, IsNil) c.Check(d, IsNil) fullPath := filepath.Join(tmpDir, "example.file") err = ioutil.WriteFile(fullPath, []byte("Example"), os.ModePerm) c.Assert(err, IsNil) d, err = LoadFile("example.file", tmpDir) c.Check(err, IsNil) c.Check(string(d), Equals, "Example") d, err = LoadFile(fullPath, tmpDir) c.Check(err, IsNil) c.Check(string(d), Equals, "Example") } func (s *configSuite) TestReadFiles(c *C) { tmpDir := c.MkDir() cfg1Path := filepath.Join(tmpDir, "cfg1.json") err := ioutil.WriteFile(cfg1Path, []byte(`{"a": 42}`), os.ModePerm) c.Assert(err, IsNil) cfg2Path := filepath.Join(tmpDir, "cfg2.json") err = ioutil.WriteFile(cfg2Path, []byte(`{"b": "x", "c_list": ["y", "z"]}`), os.ModePerm) c.Assert(err, IsNil) var cfg testConfig1 err = ReadFiles(&cfg, cfg1Path, cfg2Path) c.Assert(err, IsNil) c.Check(cfg.A, Equals, 42) c.Check(cfg.B, Equals, "x") c.Check(cfg.C, DeepEquals, []string{"y", "z"}) } func (s *configSuite) TestReadFilesErrors(c *C) { var cfg testConfig1 err := ReadFiles(1) c.Check(err, ErrorMatches, `destConfig not \*struct`) err = ReadFiles(&cfg, "non-existent") c.Check(err, ErrorMatches, "no config to read") err = ReadFiles(&cfg, "/root") c.Check(err, ErrorMatches, ".*permission denied") tmpDir := c.MkDir() err = ReadFiles(&cfg, tmpDir) c.Check(err, ErrorMatches, ".*is a directory") brokenCfgPath := filepath.Join(tmpDir, "b.json") err = ioutil.WriteFile(brokenCfgPath, []byte(`{"a"-`), os.ModePerm) c.Assert(err, IsNil) err = ReadFiles(&cfg, brokenCfgPath) c.Check(err, NotNil) } type testConfig2 struct { A int B string C []string `json:"c_list"` D ConfigTimeDuration } func (s *configSuite) TestReadFilesDefaults(c *C) { var cfg testConfig2 tmpDir := c.MkDir() emptyCfgPath := filepath.Join(tmpDir, "e.json") err := ioutil.WriteFile(emptyCfgPath, []byte("{}"), os.ModePerm) c.Assert(err, IsNil) err = ReadFilesDefaults(&cfg, map[string]interface{}{ "a": 42, "b": "foo", "c_list": []string{"bar", "baz"}, "d": "3s", }, emptyCfgPath) c.Check(err, IsNil) c.Check(cfg.A, Equals, 42) c.Check(cfg.B, Equals, "foo") c.Check(cfg.C, DeepEquals, []string{"bar", "baz"}) c.Check(cfg.D.TimeDuration(), Equals, 3*time.Second) } func (s *configSuite) TestReadFilesDefaultsError(c *C) { var cfg testConfig2 err := ReadFilesDefaults(&cfg, map[string]interface{}{ "a": make(chan int), }) c.Assert(err, NotNil) } type B struct { BFld int } type A struct { AFld int B private int } func (s *configSuite) TestTraverseStruct(c *C) { var a A var i = 1 for destField := range traverseStruct(reflect.ValueOf(&a).Elem()) { *(destField.dest.(*int)) = i i++ } c.Check(a, DeepEquals, A{1, B{2}, 0}) } func (s *configSuite) TestCompareConfig(c *C) { var cfg1 = testConfig2{ A: 1, B: "xyz", C: []string{"a", "b"}, D: ConfigTimeDuration{200 * time.Millisecond}, } var cfg2 = testConfig2{ A: 1, B: "xyz", C: []string{"a", "b"}, D: ConfigTimeDuration{200 * time.Millisecond}, } _, err := CompareConfig(cfg1, &cfg2) c.Check(err, ErrorMatches, `config1 not \*struct`) _, err = CompareConfig(&cfg1, cfg2) c.Check(err, ErrorMatches, `config2 not \*struct`) _, err = CompareConfig(&cfg1, &testConfig1{}) c.Check(err, ErrorMatches, `config1 and config2 don't have the same type`) res, err := CompareConfig(&cfg1, &cfg2) c.Assert(err, IsNil) c.Check(res, IsNil) cfg1.B = "zyx" cfg2.C = []string{"a", "B"} cfg2.D = ConfigTimeDuration{205 * time.Millisecond} res, err = CompareConfig(&cfg1, &cfg2) c.Assert(err, IsNil) c.Check(res, DeepEquals, []string{"b", "c_list", "d"}) } type testConfig3 struct { A bool B string C []string `json:"c_list"` D ConfigTimeDuration `help:"duration"` E ConfigHostPort F string } type configFlagsSuite struct{} var _ = Suite(&configFlagsSuite{}) func (s *configFlagsSuite) SetUpTest(c *C) { flag.CommandLine = flag.NewFlagSet("cmd", flag.PanicOnError) // supress outputs flag.Usage = func() { flag.PrintDefaults() } flag.CommandLine.SetOutput(ioutil.Discard) } func (s *configFlagsSuite) TestReadUsingFlags(c *C) { os.Args = []string{"cmd", "-a=1", "-b=foo", "-c_list", `["x","y"]`, "-d", "10s", "-e=localhost:80"} var cfg testConfig3 p := make(map[string]json.RawMessage) err := readUsingFlags(p, reflect.ValueOf(&cfg)) c.Assert(err, IsNil) c.Check(p, DeepEquals, map[string]json.RawMessage{ "a": json.RawMessage("true"), "b": json.RawMessage(`"foo"`), "c_list": json.RawMessage(`["x","y"]`), "d": json.RawMessage(`"10s"`), "e": json.RawMessage(`"localhost:80"`), }) } func (s *configFlagsSuite) TestReadUsingFlagsBoolError(c *C) { os.Args = []string{"cmd", "-a=zoo"} var cfg testConfig3 p := make(map[string]json.RawMessage) c.Check(func() { readUsingFlags(p, reflect.ValueOf(&cfg)) }, PanicMatches, ".*invalid boolean.*-a.*") } func (s *configFlagsSuite) TestReadFilesAndFlags(c *C) { // test pseudo file os.Args = []string{"cmd", "-b=x"} tmpDir := c.MkDir() cfgPath := filepath.Join(tmpDir, "cfg.json") err := ioutil.WriteFile(cfgPath, []byte(`{"a": 42, "c_list": ["y", "z"]}`), os.ModePerm) c.Assert(err, IsNil) var cfg testConfig1 err = ReadFiles(&cfg, cfgPath, "") c.Assert(err, IsNil) c.Check(cfg.A, Equals, 42) c.Check(cfg.B, Equals, "x") c.Check(cfg.C, DeepEquals, []string{"y", "z"}) } func (s *configFlagsSuite) TestReadFilesAndFlagsConfigAtSupport(c *C) { // test pseudo file tmpDir := c.MkDir() cfgPath := filepath.Join(tmpDir, "cfg.json") os.Args = []string{"cmd", "-a=42", fmt.Sprintf("-cfg@=%s", cfgPath)} err := ioutil.WriteFile(cfgPath, []byte(`{"b": "x", "c_list": ["y", "z"]}`), os.ModePerm) c.Assert(err, IsNil) var cfg testConfig1 err = ReadFiles(&cfg, "") c.Assert(err, IsNil) c.Check(cfg.A, Equals, 42) c.Check(cfg.B, Equals, "x") c.Check(cfg.C, DeepEquals, []string{"y", "z"}) c.Check(flag.Lookup("cfg@").Value.String(), Equals, cfgPath) } func (s *configFlagsSuite) TestReadUsingFlagsHelp(c *C) { os.Args = []string{"cmd", "-h"} buf := bytes.NewBufferString("") flag.CommandLine.Init("cmd", flag.ContinueOnError) flag.CommandLine.SetOutput(buf) var cfg testConfig3 p := map[string]json.RawMessage{ "d": json.RawMessage(`"2s"`), } readUsingFlags(p, reflect.ValueOf(&cfg)) c.Check(buf.String(), Matches, `(?s).*get config values from file.*duration.*`) } func (s *configFlagsSuite) TestReadUsingFlagsAlreadyParsed(c *C) { os.Args = []string{"cmd"} flag.Parse() var cfg struct{} p := make(map[string]json.RawMessage) err := readUsingFlags(p, reflect.ValueOf(&cfg)) c.Assert(err, ErrorMatches, "too late, flags already parsed") err = ReadFiles(&cfg, "") c.Assert(err, ErrorMatches, "too late, flags already parsed") IgnoreParsedFlags = true defer func() { IgnoreParsedFlags = false }() err = ReadFiles(&cfg, "") c.Assert(err, IsNil) } ubuntu-push-0.68+16.04.20160310.2/config/config.go0000644000015600001650000002325312670364255021523 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2014 Canonical Ltd. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ // Package config has helpers to parse and use JSON based configuration. package config import ( "encoding/json" "errors" "flag" "fmt" "io" "io/ioutil" "net" "os" "path/filepath" "reflect" "strconv" "strings" "time" ) func checkDestConfig(name string, destConfig interface{}) (reflect.Value, error) { destValue := reflect.ValueOf(destConfig) if destValue.Kind() != reflect.Ptr || destValue.Elem().Kind() != reflect.Struct { return reflect.Value{}, fmt.Errorf("%s not *struct", name) } return destValue, nil } type destField struct { fld reflect.StructField dest interface{} } func (f destField) configName() string { fld := f.fld configName := strings.Split(fld.Tag.Get("json"), ",")[0] if configName == "" { configName = strings.ToLower(fld.Name[:1]) + fld.Name[1:] } return configName } func traverseStruct(destStruct reflect.Value) <-chan destField { ch := make(chan destField) var traverse func(reflect.Value, chan<- destField) traverse = func(destStruct reflect.Value, ch chan<- destField) { structType := destStruct.Type() n := structType.NumField() for i := 0; i < n; i++ { fld := structType.Field(i) val := destStruct.Field(i) if fld.PkgPath != "" { // unexported continue } if fld.Anonymous { traverse(val, ch) continue } ch <- destField{ fld: fld, dest: val.Addr().Interface(), } } } go func() { traverse(destStruct, ch) close(ch) }() return ch } func fillDestConfig(destValue reflect.Value, p map[string]json.RawMessage) error { destStruct := destValue.Elem() for destField := range traverseStruct(destStruct) { configName := destField.configName() raw, found := p[configName] if !found { // assume all fields are mandatory for now return fmt.Errorf("missing %s", configName) } dest := destField.dest err := json.Unmarshal([]byte(raw), dest) if err != nil { return fmt.Errorf("%s: %v", configName, err) } } return nil } // ReadConfig reads a JSON configuration into destConfig which should // be a pointer to a structure. It does some more configuration // specific error checking than plain JSON decoding, and mentions // fields in errors. Configuration fields in the JSON object are // expected to start with lower case. func ReadConfig(r io.Reader, destConfig interface{}) error { destValue, err := checkDestConfig("destConfig", destConfig) if err != nil { return err } // do the parsing in two phases for better error handling var p1 map[string]json.RawMessage err = json.NewDecoder(r).Decode(&p1) if err != nil { return err } return fillDestConfig(destValue, p1) } // FromString are config holders that can be set by parsing a string. type FromString interface { SetFromString(enc string) error } // UnmarshalJSONViaString helps unmarshalling from JSON for FromString // supporting config holders. func UnmarshalJSONViaString(dest FromString, b []byte) error { var enc string err := json.Unmarshal(b, &enc) if err != nil { return err } return dest.SetFromString(enc) } // ConfigTimeDuration can hold a time.Duration in a configuration struct, // that is parsed from a string as supported by time.ParseDuration. type ConfigTimeDuration struct { time.Duration } func (ctd *ConfigTimeDuration) UnmarshalJSON(b []byte) error { return UnmarshalJSONViaString(ctd, b) } func (ctd *ConfigTimeDuration) SetFromString(enc string) error { v, err := time.ParseDuration(enc) if err != nil { return err } *ctd = ConfigTimeDuration{v} return nil } // TimeDuration returns the time.Duration held in ctd. func (ctd ConfigTimeDuration) TimeDuration() time.Duration { return ctd.Duration } // ConfigHostPort can hold a host:port string in a configuration struct. type ConfigHostPort string func (chp *ConfigHostPort) UnmarshalJSON(b []byte) error { return UnmarshalJSONViaString(chp, b) } func (chp *ConfigHostPort) SetFromString(enc string) error { _, _, err := net.SplitHostPort(enc) if err != nil { return err } *chp = ConfigHostPort(enc) return nil } // HostPort returns the host:port string held in chp. func (chp ConfigHostPort) HostPort() string { return string(chp) } // ConfigQueueSize can hold a queue size in a configuration struct. type ConfigQueueSize uint func (cqs *ConfigQueueSize) UnmarshalJSON(b []byte) error { var enc uint err := json.Unmarshal(b, &enc) if err != nil { return err } if enc == 0 { return errors.New("queue size should be > 0") } *cqs = ConfigQueueSize(enc) return nil } // QueueSize returns the queue size held in cqs. func (cqs ConfigQueueSize) QueueSize() uint { return uint(cqs) } // LoadFile reads a file possibly relative to a base dir. func LoadFile(p, baseDir string) ([]byte, error) { if p == "" { return nil, nil } if !filepath.IsAbs(p) { p = filepath.Join(baseDir, p) } return ioutil.ReadFile(p) } // used to implement getting config values with flag.Parse() type val struct { destField destField accu map[string]json.RawMessage } func (v *val) String() string { // used to show default return string(v.accu[v.destField.configName()]) } func (v *val) IsBoolFlag() bool { return v.destField.fld.Type.Kind() == reflect.Bool } func (v *val) marshalAsNeeded(s string) (json.RawMessage, error) { var toMarshal interface{} switch v.destField.dest.(type) { case *string, FromString: toMarshal = s case *bool: bit, err := strconv.ParseBool(s) if err != nil { return nil, err } toMarshal = bit default: return json.RawMessage(s), nil } return json.Marshal(toMarshal) } func (v *val) Set(s string) error { marshalled, err := v.marshalAsNeeded(s) if err != nil { return err } v.accu[v.destField.configName()] = marshalled return nil } func readOneConfig(accu map[string]json.RawMessage, cfgPath string) error { r, err := os.Open(cfgPath) if err != nil { return err } defer r.Close() err = json.NewDecoder(r).Decode(&accu) if err != nil { return err } return nil } // used to implement -cfg@= type readConfigAtVal struct { path string accu map[string]json.RawMessage } func (v *readConfigAtVal) String() string { return v.path } func (v *readConfigAtVal) Set(path string) error { v.path = path return readOneConfig(v.accu, path) } // readUsingFlags gets config values from command line flags. func readUsingFlags(accu map[string]json.RawMessage, destValue reflect.Value) error { if flag.Parsed() { if IgnoreParsedFlags { return nil } return fmt.Errorf("too late, flags already parsed") } destStruct := destValue.Elem() for destField := range traverseStruct(destStruct) { help := destField.fld.Tag.Get("help") flag.Var(&val{destField, accu}, destField.configName(), help) } flag.Var(&readConfigAtVal{"", accu}, "cfg@", "get config values from file") flag.Parse() return nil } // IgnoreParsedFlags will just have ReadFiles ignore if the // command line was already parsed. var IgnoreParsedFlags = false // ReadFilesDefaults reads configuration from a set of files. The // string "" can be used as a pseudo file-path, it will // consider command line flags, invoking flag.Parse(). Among those the // flag -cfg@=FILE can be used to get further config values from FILE. // Defaults for fields can be given through a map[string]interface{}. func ReadFilesDefaults(destConfig interface{}, defls map[string]interface{}, cfgFpaths ...string) error { destValue, err := checkDestConfig("destConfig", destConfig) if err != nil { return err } // do the parsing in two phases for better error handling p1 := make(map[string]json.RawMessage) for field, value := range defls { b, err := json.Marshal(value) if err != nil { return err } p1[field] = json.RawMessage(b) } readOne := false for _, cfgPath := range cfgFpaths { if cfgPath == "" { err := readUsingFlags(p1, destValue) if err != nil { return err } readOne = true continue } if _, err := os.Stat(cfgPath); err == nil { err := readOneConfig(p1, cfgPath) if err != nil { return err } readOne = true } } if !readOne { return fmt.Errorf("no config to read") } return fillDestConfig(destValue, p1) } // ReadFiles reads configuration from a set of files exactly like // ReadFilesDefaults but no defaults can be given making all fields // mandatory. func ReadFiles(destConfig interface{}, cfgFpaths ...string) error { return ReadFilesDefaults(destConfig, nil, cfgFpaths...) } // CompareConfigs compares the two given configuration structures. It returns a list of differing fields or nil if the config contents are the same. func CompareConfig(config1, config2 interface{}) ([]string, error) { v1, err := checkDestConfig("config1", config1) if err != nil { return nil, err } v2, err := checkDestConfig("config2", config2) if err != nil { return nil, err } if v1.Type() != v2.Type() { return nil, errors.New("config1 and config2 don't have the same type") } fields1 := traverseStruct(v1.Elem()) fields2 := traverseStruct(v2.Elem()) diff := make([]string, 0) for { d1 := <-fields1 d2 := <-fields2 if d1.dest == nil { break } if !reflect.DeepEqual(d1.dest, d2.dest) { diff = append(diff, d1.configName()) } } if len(diff) != 0 { return diff, nil } return nil, nil } ubuntu-push-0.68+16.04.20160310.2/http13client/0000755000015600001650000000000012670364532020775 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/http13client/doc.go0000644000015600001650000000305112670364255022072 0ustar pbuserpbgroup00000000000000// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* Package http contains the client subset of go 1.3 development net/http. Get, Head, Post, and PostForm make HTTP (or HTTPS) requests: resp, err := http.Get("http://example.com/") ... resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf) ... resp, err := http.PostForm("http://example.com/form", url.Values{"key": {"Value"}, "id": {"123"}}) The client must close the response body when finished with it: resp, err := http.Get("http://example.com/") if err != nil { // handle error } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) // ... For control over HTTP client headers, redirect policy, and other settings, create a Client: client := &http.Client{ CheckRedirect: redirectPolicyFunc, } resp, err := client.Get("http://example.com") // ... req, err := http.NewRequest("GET", "http://example.com", nil) // ... req.Header.Add("If-None-Match", `W/"wyzzy"`) resp, err := client.Do(req) // ... For control over proxies, TLS configuration, keep-alives, compression, and other settings, create a Transport: tr := &http.Transport{ TLSClientConfig: &tls.Config{RootCAs: pool}, DisableCompression: true, } client := &http.Client{Transport: tr} resp, err := client.Get("https://example.com") Clients and Transports are safe for concurrent use by multiple goroutines and for efficiency should only be created once and re-used. */ package http ubuntu-push-0.68+16.04.20160310.2/http13client/export_test.go0000644000015600001650000000234012670364255023705 0ustar pbuserpbgroup00000000000000// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Bridge package to expose http internals to tests in the http_test // package. package http import ( "net" ) func NewLoggingConn(baseName string, c net.Conn) net.Conn { return newLoggingConn(baseName, c) } func (t *Transport) NumPendingRequestsForTesting() int { t.reqMu.Lock() defer t.reqMu.Unlock() return len(t.reqCanceler) } func (t *Transport) IdleConnKeysForTesting() (keys []string) { keys = make([]string, 0) t.idleMu.Lock() defer t.idleMu.Unlock() if t.idleConn == nil { return } for key := range t.idleConn { keys = append(keys, key.String()) } return } func (t *Transport) IdleConnCountForTesting(cacheKey string) int { t.idleMu.Lock() defer t.idleMu.Unlock() if t.idleConn == nil { return 0 } for k, conns := range t.idleConn { if k.String() == cacheKey { return len(conns) } } return 0 } func (t *Transport) IdleConnChMapSizeForTesting() int { t.idleMu.Lock() defer t.idleMu.Unlock() return len(t.idleConnCh) } func ResetCachedEnvironment() { httpProxyEnv.reset() noProxyEnv.reset() } var DefaultUserAgent = defaultUserAgent ubuntu-push-0.68+16.04.20160310.2/http13client/chunked_test.go0000644000015600001650000001005312670364255024005 0ustar pbuserpbgroup00000000000000// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This code is duplicated in net/http and net/http/httputil. // Please make any changes in both files. package http import ( "bufio" "bytes" "fmt" "io" "io/ioutil" "strings" "testing" ) func TestChunk(t *testing.T) { var b bytes.Buffer w := newChunkedWriter(&b) const chunk1 = "hello, " const chunk2 = "world! 0123456789abcdef" w.Write([]byte(chunk1)) w.Write([]byte(chunk2)) w.Close() if g, e := b.String(), "7\r\nhello, \r\n17\r\nworld! 0123456789abcdef\r\n0\r\n"; g != e { t.Fatalf("chunk writer wrote %q; want %q", g, e) } r := newChunkedReader(&b) data, err := ioutil.ReadAll(r) if err != nil { t.Logf(`data: "%s"`, data) t.Fatalf("ReadAll from reader: %v", err) } if g, e := string(data), chunk1+chunk2; g != e { t.Errorf("chunk reader read %q; want %q", g, e) } } func TestChunkReadMultiple(t *testing.T) { // Bunch of small chunks, all read together. { var b bytes.Buffer w := newChunkedWriter(&b) w.Write([]byte("foo")) w.Write([]byte("bar")) w.Close() r := newChunkedReader(&b) buf := make([]byte, 10) n, err := r.Read(buf) if n != 6 || err != io.EOF { t.Errorf("Read = %d, %v; want 6, EOF", n, err) } buf = buf[:n] if string(buf) != "foobar" { t.Errorf("Read = %q; want %q", buf, "foobar") } } // One big chunk followed by a little chunk, but the small bufio.Reader size // should prevent the second chunk header from being read. { var b bytes.Buffer w := newChunkedWriter(&b) // fillBufChunk is 11 bytes + 3 bytes header + 2 bytes footer = 16 bytes, // the same as the bufio ReaderSize below (the minimum), so even // though we're going to try to Read with a buffer larger enough to also // receive "foo", the second chunk header won't be read yet. const fillBufChunk = "0123456789a" const shortChunk = "foo" w.Write([]byte(fillBufChunk)) w.Write([]byte(shortChunk)) w.Close() r := newChunkedReader(bufio.NewReaderSize(&b, 16)) buf := make([]byte, len(fillBufChunk)+len(shortChunk)) n, err := r.Read(buf) if n != len(fillBufChunk) || err != nil { t.Errorf("Read = %d, %v; want %d, nil", n, err, len(fillBufChunk)) } buf = buf[:n] if string(buf) != fillBufChunk { t.Errorf("Read = %q; want %q", buf, fillBufChunk) } n, err = r.Read(buf) if n != len(shortChunk) || err != io.EOF { t.Errorf("Read = %d, %v; want %d, EOF", n, err, len(shortChunk)) } } // And test that we see an EOF chunk, even though our buffer is already full: { r := newChunkedReader(bufio.NewReader(strings.NewReader("3\r\nfoo\r\n0\r\n"))) buf := make([]byte, 3) n, err := r.Read(buf) if n != 3 || err != io.EOF { t.Errorf("Read = %d, %v; want 3, EOF", n, err) } if string(buf) != "foo" { t.Errorf("buf = %q; want foo", buf) } } } func TestChunkReaderAllocs(t *testing.T) { if testing.Short() { t.Skip("skipping in short mode") } var buf bytes.Buffer w := newChunkedWriter(&buf) a, b, c := []byte("aaaaaa"), []byte("bbbbbbbbbbbb"), []byte("cccccccccccccccccccccccc") w.Write(a) w.Write(b) w.Write(c) w.Close() readBuf := make([]byte, len(a)+len(b)+len(c)+1) byter := bytes.NewReader(buf.Bytes()) bufr := bufio.NewReader(byter) mallocs := testing.AllocsPerRun(100, func() { byter.Seek(0, 0) bufr.Reset(byter) r := newChunkedReader(bufr) n, err := io.ReadFull(r, readBuf) if n != len(readBuf)-1 { t.Fatalf("read %d bytes; want %d", n, len(readBuf)-1) } if err != io.ErrUnexpectedEOF { t.Fatalf("read error = %v; want ErrUnexpectedEOF", err) } }) if mallocs > 1.5 { t.Errorf("mallocs = %v; want 1", mallocs) } } func TestParseHexUint(t *testing.T) { for i := uint64(0); i <= 1234; i++ { line := []byte(fmt.Sprintf("%x", i)) got, err := parseHexUint(line) if err != nil { t.Fatalf("on %d: %v", i, err) } if got != i { t.Errorf("for input %q = %d; want %d", line, got, i) } } _, err := parseHexUint([]byte("bogus")) if err == nil { t.Error("expected error on bogus input") } } ubuntu-push-0.68+16.04.20160310.2/http13client/header_test.go0000644000015600001650000001153012670364255023615 0ustar pbuserpbgroup00000000000000// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "bytes" "net/http" "runtime" "testing" "time" ) var headerWriteTests = []struct { h http.Header exclude map[string]bool expected string }{ {http.Header{}, nil, ""}, { http.Header{ "Content-Type": {"text/html; charset=UTF-8"}, "Content-Length": {"0"}, }, nil, "Content-Length: 0\r\nContent-Type: text/html; charset=UTF-8\r\n", }, { http.Header{ "Content-Length": {"0", "1", "2"}, }, nil, "Content-Length: 0\r\nContent-Length: 1\r\nContent-Length: 2\r\n", }, { http.Header{ "Expires": {"-1"}, "Content-Length": {"0"}, "Content-Encoding": {"gzip"}, }, map[string]bool{"Content-Length": true}, "Content-Encoding: gzip\r\nExpires: -1\r\n", }, { http.Header{ "Expires": {"-1"}, "Content-Length": {"0", "1", "2"}, "Content-Encoding": {"gzip"}, }, map[string]bool{"Content-Length": true}, "Content-Encoding: gzip\r\nExpires: -1\r\n", }, { http.Header{ "Expires": {"-1"}, "Content-Length": {"0"}, "Content-Encoding": {"gzip"}, }, map[string]bool{"Content-Length": true, "Expires": true, "Content-Encoding": true}, "", }, { http.Header{ "Nil": nil, "Empty": {}, "Blank": {""}, "Double-Blank": {"", ""}, }, nil, "Blank: \r\nDouble-Blank: \r\nDouble-Blank: \r\n", }, // Tests header sorting when over the insertion sort threshold side: { http.Header{ "k1": {"1a", "1b"}, "k2": {"2a", "2b"}, "k3": {"3a", "3b"}, "k4": {"4a", "4b"}, "k5": {"5a", "5b"}, "k6": {"6a", "6b"}, "k7": {"7a", "7b"}, "k8": {"8a", "8b"}, "k9": {"9a", "9b"}, }, map[string]bool{"k5": true}, "k1: 1a\r\nk1: 1b\r\nk2: 2a\r\nk2: 2b\r\nk3: 3a\r\nk3: 3b\r\n" + "k4: 4a\r\nk4: 4b\r\nk6: 6a\r\nk6: 6b\r\n" + "k7: 7a\r\nk7: 7b\r\nk8: 8a\r\nk8: 8b\r\nk9: 9a\r\nk9: 9b\r\n", }, } func TestHeaderWrite(t *testing.T) { var buf bytes.Buffer for i, test := range headerWriteTests { test.h.WriteSubset(&buf, test.exclude) if buf.String() != test.expected { t.Errorf("#%d:\n got: %q\nwant: %q", i, buf.String(), test.expected) } buf.Reset() } } var parseTimeTests = []struct { h http.Header err bool }{ {http.Header{"Date": {""}}, true}, {http.Header{"Date": {"invalid"}}, true}, {http.Header{"Date": {"1994-11-06T08:49:37Z00:00"}}, true}, {http.Header{"Date": {"Sun, 06 Nov 1994 08:49:37 GMT"}}, false}, {http.Header{"Date": {"Sunday, 06-Nov-94 08:49:37 GMT"}}, false}, {http.Header{"Date": {"Sun Nov 6 08:49:37 1994"}}, false}, } func TestParseTime(t *testing.T) { expect := time.Date(1994, 11, 6, 8, 49, 37, 0, time.UTC) for i, test := range parseTimeTests { d, err := http.ParseTime(test.h.Get("Date")) if err != nil { if !test.err { t.Errorf("#%d:\n got err: %v", i, err) } continue } if test.err { t.Errorf("#%d:\n should err", i) continue } if !expect.Equal(d) { t.Errorf("#%d:\n got: %v\nwant: %v", i, d, expect) } } } type hasTokenTest struct { header string token string want bool } var hasTokenTests = []hasTokenTest{ {"", "", false}, {"", "foo", false}, {"foo", "foo", true}, {"foo ", "foo", true}, {" foo", "foo", true}, {" foo ", "foo", true}, {"foo,bar", "foo", true}, {"bar,foo", "foo", true}, {"bar, foo", "foo", true}, {"bar,foo, baz", "foo", true}, {"bar, foo,baz", "foo", true}, {"bar,foo, baz", "foo", true}, {"bar, foo, baz", "foo", true}, {"FOO", "foo", true}, {"FOO ", "foo", true}, {" FOO", "foo", true}, {" FOO ", "foo", true}, {"FOO,BAR", "foo", true}, {"BAR,FOO", "foo", true}, {"BAR, FOO", "foo", true}, {"BAR,FOO, baz", "foo", true}, {"BAR, FOO,BAZ", "foo", true}, {"BAR,FOO, BAZ", "foo", true}, {"BAR, FOO, BAZ", "foo", true}, {"foobar", "foo", false}, {"barfoo ", "foo", false}, } func TestHasToken(t *testing.T) { for _, tt := range hasTokenTests { if hasToken(tt.header, tt.token) != tt.want { t.Errorf("hasToken(%q, %q) = %v; want %v", tt.header, tt.token, !tt.want, tt.want) } } } var testHeader = http.Header{ "Content-Length": {"123"}, "Content-Type": {"text/plain"}, "Date": {"some date at some time Z"}, "Server": {DefaultUserAgent}, } var buf bytes.Buffer func BenchmarkHeaderWriteSubset(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { buf.Reset() testHeader.WriteSubset(&buf, nil) } } func TestHeaderWriteSubsetAllocs(t *testing.T) { if testing.Short() { t.Skip("skipping alloc test in short mode") } /*if raceEnabled { t.Skip("skipping test under race detector") }*/ if runtime.GOMAXPROCS(0) > 1 { t.Skip("skipping; GOMAXPROCS>1") } n := testing.AllocsPerRun(100, func() { buf.Reset() testHeader.WriteSubset(&buf, nil) }) if n > 0 { t.Errorf("allocs = %g; want 0", n) } } ubuntu-push-0.68+16.04.20160310.2/http13client/transport_test.go0000644000015600001650000015620312670364255024430 0ustar pbuserpbgroup00000000000000// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Tests for transport.go package http_test import ( "bufio" "bytes" "compress/gzip" "crypto/rand" "crypto/tls" "errors" "fmt" "io" "io/ioutil" . "launchpad.net/ubuntu-push/http13client" "log" "net" "net/http" "net/http/httptest" "net/url" "os" "runtime" "strconv" "strings" "sync" "testing" "time" ) // TODO: test 5 pipelined requests with responses: 1) OK, 2) OK, Connection: Close // and then verify that the final 2 responses get errors back. // hostPortHandler writes back the client's "host:port". var hostPortHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.FormValue("close") == "true" { w.Header().Set("Connection", "close") } w.Write([]byte(r.RemoteAddr)) }) // testCloseConn is a net.Conn tracked by a testConnSet. type testCloseConn struct { net.Conn set *testConnSet } func (c *testCloseConn) Close() error { c.set.remove(c) return c.Conn.Close() } // testConnSet tracks a set of TCP connections and whether they've // been closed. type testConnSet struct { t *testing.T mu sync.Mutex // guards closed and list closed map[net.Conn]bool list []net.Conn // in order created } func (tcs *testConnSet) insert(c net.Conn) { tcs.mu.Lock() defer tcs.mu.Unlock() tcs.closed[c] = false tcs.list = append(tcs.list, c) } func (tcs *testConnSet) remove(c net.Conn) { tcs.mu.Lock() defer tcs.mu.Unlock() tcs.closed[c] = true } // some tests use this to manage raw tcp connections for later inspection func makeTestDial(t *testing.T) (*testConnSet, func(n, addr string) (net.Conn, error)) { connSet := &testConnSet{ t: t, closed: make(map[net.Conn]bool), } dial := func(n, addr string) (net.Conn, error) { c, err := net.Dial(n, addr) if err != nil { return nil, err } tc := &testCloseConn{c, connSet} connSet.insert(tc) return tc, nil } return connSet, dial } func (tcs *testConnSet) check(t *testing.T) { tcs.mu.Lock() defer tcs.mu.Unlock() for i := 4; i >= 0; i-- { for i, c := range tcs.list { if tcs.closed[c] { continue } if i != 0 { tcs.mu.Unlock() time.Sleep(50 * time.Millisecond) tcs.mu.Lock() continue } t.Errorf("TCP connection #%d, %p (of %d total) was not closed", i+1, c, len(tcs.list)) } } } // Two subsequent requests and verify their response is the same. // The response from the server is our own IP:port func TestTransportKeepAlives(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(hostPortHandler) defer ts.Close() for _, disableKeepAlive := range []bool{false, true} { tr := &Transport{DisableKeepAlives: disableKeepAlive} defer tr.CloseIdleConnections() c := &Client{Transport: tr} fetch := func(n int) string { res, err := c.Get(ts.URL) if err != nil { t.Fatalf("error in disableKeepAlive=%v, req #%d, GET: %v", disableKeepAlive, n, err) } body, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatalf("error in disableKeepAlive=%v, req #%d, ReadAll: %v", disableKeepAlive, n, err) } return string(body) } body1 := fetch(1) body2 := fetch(2) bodiesDiffer := body1 != body2 if bodiesDiffer != disableKeepAlive { t.Errorf("error in disableKeepAlive=%v. unexpected bodiesDiffer=%v; body1=%q; body2=%q", disableKeepAlive, bodiesDiffer, body1, body2) } } } func TestTransportConnectionCloseOnResponse(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(hostPortHandler) defer ts.Close() connSet, testDial := makeTestDial(t) for _, connectionClose := range []bool{false, true} { tr := &Transport{ Dial: testDial, } c := &Client{Transport: tr} fetch := func(n int) string { req := new(Request) var err error req.URL, err = url.Parse(ts.URL + fmt.Sprintf("/?close=%v", connectionClose)) if err != nil { t.Fatalf("URL parse error: %v", err) } req.Method = "GET" req.Proto = "HTTP/1.1" req.ProtoMajor = 1 req.ProtoMinor = 1 res, err := c.Do(req) if err != nil { t.Fatalf("error in connectionClose=%v, req #%d, Do: %v", connectionClose, n, err) } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatalf("error in connectionClose=%v, req #%d, ReadAll: %v", connectionClose, n, err) } return string(body) } body1 := fetch(1) body2 := fetch(2) bodiesDiffer := body1 != body2 if bodiesDiffer != connectionClose { t.Errorf("error in connectionClose=%v. unexpected bodiesDiffer=%v; body1=%q; body2=%q", connectionClose, bodiesDiffer, body1, body2) } tr.CloseIdleConnections() } connSet.check(t) } func TestTransportConnectionCloseOnRequest(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(hostPortHandler) defer ts.Close() connSet, testDial := makeTestDial(t) for _, connectionClose := range []bool{false, true} { tr := &Transport{ Dial: testDial, } c := &Client{Transport: tr} fetch := func(n int) string { req := new(Request) var err error req.URL, err = url.Parse(ts.URL) if err != nil { t.Fatalf("URL parse error: %v", err) } req.Method = "GET" req.Proto = "HTTP/1.1" req.ProtoMajor = 1 req.ProtoMinor = 1 req.Close = connectionClose res, err := c.Do(req) if err != nil { t.Fatalf("error in connectionClose=%v, req #%d, Do: %v", connectionClose, n, err) } body, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatalf("error in connectionClose=%v, req #%d, ReadAll: %v", connectionClose, n, err) } return string(body) } body1 := fetch(1) body2 := fetch(2) bodiesDiffer := body1 != body2 if bodiesDiffer != connectionClose { t.Errorf("error in connectionClose=%v. unexpected bodiesDiffer=%v; body1=%q; body2=%q", connectionClose, bodiesDiffer, body1, body2) } tr.CloseIdleConnections() } connSet.check(t) } func TestTransportIdleCacheKeys(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(hostPortHandler) defer ts.Close() tr := &Transport{DisableKeepAlives: false} c := &Client{Transport: tr} if e, g := 0, len(tr.IdleConnKeysForTesting()); e != g { t.Errorf("After CloseIdleConnections expected %d idle conn cache keys; got %d", e, g) } resp, err := c.Get(ts.URL) if err != nil { t.Error(err) } ioutil.ReadAll(resp.Body) keys := tr.IdleConnKeysForTesting() if e, g := 1, len(keys); e != g { t.Fatalf("After Get expected %d idle conn cache keys; got %d", e, g) } if e := "|http|" + ts.Listener.Addr().String(); keys[0] != e { t.Errorf("Expected idle cache key %q; got %q", e, keys[0]) } tr.CloseIdleConnections() if e, g := 0, len(tr.IdleConnKeysForTesting()); e != g { t.Errorf("After CloseIdleConnections expected %d idle conn cache keys; got %d", e, g) } } // Tests that the HTTP transport re-uses connections when a client // reads to the end of a response Body without closing it. func TestTransportReadToEndReusesConn(t *testing.T) { defer afterTest(t) const msg = "foobar" var addrSeen map[string]int ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { addrSeen[r.RemoteAddr]++ if r.URL.Path == "/chunked/" { w.WriteHeader(200) w.(http.Flusher).Flush() } else { w.Header().Set("Content-Type", strconv.Itoa(len(msg))) w.WriteHeader(200) } w.Write([]byte(msg)) })) defer ts.Close() buf := make([]byte, len(msg)) for pi, path := range []string{"/content-length/", "/chunked/"} { wantLen := []int{len(msg), -1}[pi] addrSeen = make(map[string]int) for i := 0; i < 3; i++ { res, err := Get(ts.URL + path) if err != nil { t.Errorf("Get %s: %v", path, err) continue } // We want to close this body eventually (before the // defer afterTest at top runs), but not before the // len(addrSeen) check at the bottom of this test, // since Closing this early in the loop would risk // making connections be re-used for the wrong reason. defer res.Body.Close() if res.ContentLength != int64(wantLen) { t.Errorf("%s res.ContentLength = %d; want %d", path, res.ContentLength, wantLen) } n, err := res.Body.Read(buf) if n != len(msg) || err != io.EOF { t.Errorf("%s Read = %v, %v; want %d, EOF", path, n, err, len(msg)) } } if len(addrSeen) != 1 { t.Errorf("for %s, server saw %d distinct client addresses; want 1", path, len(addrSeen)) } } } func TestTransportMaxPerHostIdleConns(t *testing.T) { defer afterTest(t) resch := make(chan string) gotReq := make(chan bool) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotReq <- true msg := <-resch _, err := w.Write([]byte(msg)) if err != nil { t.Fatalf("Write: %v", err) } })) defer ts.Close() maxIdleConns := 2 tr := &Transport{DisableKeepAlives: false, MaxIdleConnsPerHost: maxIdleConns} c := &Client{Transport: tr} // Start 3 outstanding requests and wait for the server to get them. // Their responses will hang until we write to resch, though. donech := make(chan bool) doReq := func() { resp, err := c.Get(ts.URL) if err != nil { t.Error(err) return } if _, err := ioutil.ReadAll(resp.Body); err != nil { t.Errorf("ReadAll: %v", err) return } donech <- true } go doReq() <-gotReq go doReq() <-gotReq go doReq() <-gotReq if e, g := 0, len(tr.IdleConnKeysForTesting()); e != g { t.Fatalf("Before writes, expected %d idle conn cache keys; got %d", e, g) } resch <- "res1" <-donech keys := tr.IdleConnKeysForTesting() if e, g := 1, len(keys); e != g { t.Fatalf("after first response, expected %d idle conn cache keys; got %d", e, g) } cacheKey := "|http|" + ts.Listener.Addr().String() if keys[0] != cacheKey { t.Fatalf("Expected idle cache key %q; got %q", cacheKey, keys[0]) } if e, g := 1, tr.IdleConnCountForTesting(cacheKey); e != g { t.Errorf("after first response, expected %d idle conns; got %d", e, g) } resch <- "res2" <-donech if e, g := 2, tr.IdleConnCountForTesting(cacheKey); e != g { t.Errorf("after second response, expected %d idle conns; got %d", e, g) } resch <- "res3" <-donech if e, g := maxIdleConns, tr.IdleConnCountForTesting(cacheKey); e != g { t.Errorf("after third response, still expected %d idle conns; got %d", e, g) } } func TestTransportServerClosingUnexpectedly(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(hostPortHandler) defer ts.Close() tr := &Transport{} c := &Client{Transport: tr} fetch := func(n, retries int) string { condFatalf := func(format string, arg ...interface{}) { if retries <= 0 { t.Fatalf(format, arg...) } t.Logf("retrying shortly after expected error: "+format, arg...) time.Sleep(time.Second / time.Duration(retries)) } for retries >= 0 { retries-- res, err := c.Get(ts.URL) if err != nil { condFatalf("error in req #%d, GET: %v", n, err) continue } body, err := ioutil.ReadAll(res.Body) if err != nil { condFatalf("error in req #%d, ReadAll: %v", n, err) continue } res.Body.Close() return string(body) } panic("unreachable") } body1 := fetch(1, 0) body2 := fetch(2, 0) ts.CloseClientConnections() // surprise! // This test has an expected race. Sleeping for 25 ms prevents // it on most fast machines, causing the next fetch() call to // succeed quickly. But if we do get errors, fetch() will retry 5 // times with some delays between. time.Sleep(25 * time.Millisecond) body3 := fetch(3, 5) if body1 != body2 { t.Errorf("expected body1 and body2 to be equal") } if body2 == body3 { t.Errorf("expected body2 and body3 to be different") } } // Test for http://golang.org/issue/2616 (appropriate issue number) // This fails pretty reliably with GOMAXPROCS=100 or something high. func TestStressSurpriseServerCloses(t *testing.T) { defer afterTest(t) if testing.Short() { t.Skip("skipping test in short mode") } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", "5") w.Header().Set("Content-Type", "text/plain") w.Write([]byte("Hello")) w.(http.Flusher).Flush() conn, buf, _ := w.(http.Hijacker).Hijack() buf.Flush() conn.Close() })) defer ts.Close() tr := &Transport{DisableKeepAlives: false} c := &Client{Transport: tr} // Do a bunch of traffic from different goroutines. Send to activityc // after each request completes, regardless of whether it failed. const ( numClients = 50 reqsPerClient = 250 ) activityc := make(chan bool) for i := 0; i < numClients; i++ { go func() { for i := 0; i < reqsPerClient; i++ { res, err := c.Get(ts.URL) if err == nil { // We expect errors since the server is // hanging up on us after telling us to // send more requests, so we don't // actually care what the error is. // But we want to close the body in cases // where we won the race. res.Body.Close() } activityc <- true } }() } // Make sure all the request come back, one way or another. for i := 0; i < numClients*reqsPerClient; i++ { select { case <-activityc: case <-time.After(5 * time.Second): t.Fatalf("presumed deadlock; no HTTP client activity seen in awhile") } } } // TestTransportHeadResponses verifies that we deal with Content-Lengths // with no bodies properly func TestTransportHeadResponses(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "HEAD" { panic("expected HEAD; got " + r.Method) } w.Header().Set("Content-Length", "123") w.WriteHeader(200) })) defer ts.Close() tr := &Transport{DisableKeepAlives: false} c := &Client{Transport: tr} for i := 0; i < 2; i++ { res, err := c.Head(ts.URL) if err != nil { t.Errorf("error on loop %d: %v", i, err) continue } if e, g := "123", res.Header.Get("Content-Length"); e != g { t.Errorf("loop %d: expected Content-Length header of %q, got %q", i, e, g) } if e, g := int64(123), res.ContentLength; e != g { t.Errorf("loop %d: expected res.ContentLength of %v, got %v", i, e, g) } if all, err := ioutil.ReadAll(res.Body); err != nil { t.Errorf("loop %d: Body ReadAll: %v", i, err) } else if len(all) != 0 { t.Errorf("Bogus body %q", all) } } } // TestTransportHeadChunkedResponse verifies that we ignore chunked transfer-encoding // on responses to HEAD requests. func TestTransportHeadChunkedResponse(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "HEAD" { panic("expected HEAD; got " + r.Method) } w.Header().Set("Transfer-Encoding", "chunked") // client should ignore w.Header().Set("x-client-ipport", r.RemoteAddr) w.WriteHeader(200) })) defer ts.Close() tr := &Transport{DisableKeepAlives: false} c := &Client{Transport: tr} res1, err := c.Head(ts.URL) if err != nil { t.Fatalf("request 1 error: %v", err) } res2, err := c.Head(ts.URL) if err != nil { t.Fatalf("request 2 error: %v", err) } if v1, v2 := res1.Header.Get("x-client-ipport"), res2.Header.Get("x-client-ipport"); v1 != v2 { t.Errorf("ip/ports differed between head requests: %q vs %q", v1, v2) } } var roundTripTests = []struct { accept string expectAccept string compressed bool }{ // Requests with no accept-encoding header use transparent compression {"", "gzip", false}, // Requests with other accept-encoding should pass through unmodified {"foo", "foo", false}, // Requests with accept-encoding == gzip should be passed through {"gzip", "gzip", true}, } // Test that the modification made to the Request by the RoundTripper is cleaned up func TestRoundTripGzip(t *testing.T) { defer afterTest(t) const responseBody = "test response body" ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { accept := req.Header.Get("Accept-Encoding") if expect := req.FormValue("expect_accept"); accept != expect { t.Errorf("in handler, test %v: Accept-Encoding = %q, want %q", req.FormValue("testnum"), accept, expect) } if accept == "gzip" { rw.Header().Set("Content-Encoding", "gzip") gz := gzip.NewWriter(rw) gz.Write([]byte(responseBody)) gz.Close() } else { rw.Header().Set("Content-Encoding", accept) rw.Write([]byte(responseBody)) } })) defer ts.Close() for i, test := range roundTripTests { // Test basic request (no accept-encoding) req, _ := NewRequest("GET", fmt.Sprintf("%s/?testnum=%d&expect_accept=%s", ts.URL, i, test.expectAccept), nil) if test.accept != "" { req.Header.Set("Accept-Encoding", test.accept) } res, err := DefaultTransport.RoundTrip(req) var body []byte if test.compressed { var r *gzip.Reader r, err = gzip.NewReader(res.Body) if err != nil { t.Errorf("%d. gzip NewReader: %v", i, err) continue } body, err = ioutil.ReadAll(r) res.Body.Close() } else { body, err = ioutil.ReadAll(res.Body) } if err != nil { t.Errorf("%d. Error: %q", i, err) continue } if g, e := string(body), responseBody; g != e { t.Errorf("%d. body = %q; want %q", i, g, e) } if g, e := req.Header.Get("Accept-Encoding"), test.accept; g != e { t.Errorf("%d. Accept-Encoding = %q; want %q (it was mutated, in violation of RoundTrip contract)", i, g, e) } if g, e := res.Header.Get("Content-Encoding"), test.accept; g != e { t.Errorf("%d. Content-Encoding = %q; want %q", i, g, e) } } } func TestTransportGzip(t *testing.T) { defer afterTest(t) const testString = "The test string aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" const nRandBytes = 1024 * 1024 ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if req.Method == "HEAD" { if g := req.Header.Get("Accept-Encoding"); g != "" { t.Errorf("HEAD request sent with Accept-Encoding of %q; want none", g) } return } if g, e := req.Header.Get("Accept-Encoding"), "gzip"; g != e { t.Errorf("Accept-Encoding = %q, want %q", g, e) } rw.Header().Set("Content-Encoding", "gzip") var w io.Writer = rw var buf bytes.Buffer if req.FormValue("chunked") == "0" { w = &buf defer io.Copy(rw, &buf) defer func() { rw.Header().Set("Content-Length", strconv.Itoa(buf.Len())) }() } gz := gzip.NewWriter(w) gz.Write([]byte(testString)) if req.FormValue("body") == "large" { io.CopyN(gz, rand.Reader, nRandBytes) } gz.Close() })) defer ts.Close() for _, chunked := range []string{"1", "0"} { c := &Client{Transport: &Transport{}} // First fetch something large, but only read some of it. res, err := c.Get(ts.URL + "/?body=large&chunked=" + chunked) if err != nil { t.Fatalf("large get: %v", err) } buf := make([]byte, len(testString)) n, err := io.ReadFull(res.Body, buf) if err != nil { t.Fatalf("partial read of large response: size=%d, %v", n, err) } if e, g := testString, string(buf); e != g { t.Errorf("partial read got %q, expected %q", g, e) } res.Body.Close() // Read on the body, even though it's closed n, err = res.Body.Read(buf) if n != 0 || err == nil { t.Errorf("expected error post-closed large Read; got = %d, %v", n, err) } // Then something small. res, err = c.Get(ts.URL + "/?chunked=" + chunked) if err != nil { t.Fatal(err) } body, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatal(err) } if g, e := string(body), testString; g != e { t.Fatalf("body = %q; want %q", g, e) } if g, e := res.Header.Get("Content-Encoding"), ""; g != e { t.Fatalf("Content-Encoding = %q; want %q", g, e) } // Read on the body after it's been fully read: n, err = res.Body.Read(buf) if n != 0 || err == nil { t.Errorf("expected Read error after exhausted reads; got %d, %v", n, err) } res.Body.Close() n, err = res.Body.Read(buf) if n != 0 || err == nil { t.Errorf("expected Read error after Close; got %d, %v", n, err) } } // And a HEAD request too, because they're always weird. c := &Client{Transport: &Transport{}} res, err := c.Head(ts.URL) if err != nil { t.Fatalf("Head: %v", err) } if res.StatusCode != 200 { t.Errorf("Head status=%d; want=200", res.StatusCode) } } func TestTransportProxy(t *testing.T) { defer afterTest(t) ch := make(chan string, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ch <- "real server" })) defer ts.Close() proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ch <- "proxy for " + r.URL.String() })) defer proxy.Close() pu, err := url.Parse(proxy.URL) if err != nil { t.Fatal(err) } c := &Client{Transport: &Transport{Proxy: ProxyURL(pu)}} c.Head(ts.URL) got := <-ch want := "proxy for " + ts.URL + "/" if got != want { t.Errorf("want %q, got %q", want, got) } } // TestTransportGzipRecursive sends a gzip quine and checks that the // client gets the same value back. This is more cute than anything, // but checks that we don't recurse forever, and checks that // Content-Encoding is removed. func TestTransportGzipRecursive(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Encoding", "gzip") w.Write(rgz) })) defer ts.Close() c := &Client{Transport: &Transport{}} res, err := c.Get(ts.URL) if err != nil { t.Fatal(err) } body, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatal(err) } if !bytes.Equal(body, rgz) { t.Fatalf("Incorrect result from recursive gz:\nhave=%x\nwant=%x", body, rgz) } if g, e := res.Header.Get("Content-Encoding"), ""; g != e { t.Fatalf("Content-Encoding = %q; want %q", g, e) } } // golang.org/issue/7750: request fails when server replies with // a short gzip body func TestTransportGzipShort(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Encoding", "gzip") w.Write([]byte{0x1f, 0x8b}) })) defer ts.Close() tr := &Transport{} defer tr.CloseIdleConnections() c := &Client{Transport: tr} res, err := c.Get(ts.URL) if err != nil { t.Fatal(err) } defer res.Body.Close() _, err = ioutil.ReadAll(res.Body) if err == nil { t.Fatal("Expect an error from reading a body.") } if err != io.ErrUnexpectedEOF { t.Errorf("ReadAll error = %v; want io.ErrUnexpectedEOF", err) } } // tests that persistent goroutine connections shut down when no longer desired. func TestTransportPersistConnLeak(t *testing.T) { if runtime.GOOS == "plan9" { t.Skip("skipping test; see http://golang.org/issue/7237") } defer afterTest(t) gotReqCh := make(chan bool) unblockCh := make(chan bool) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotReqCh <- true <-unblockCh w.Header().Set("Content-Length", "0") w.WriteHeader(204) })) defer ts.Close() tr := &Transport{} c := &Client{Transport: tr} n0 := runtime.NumGoroutine() const numReq = 25 didReqCh := make(chan bool) for i := 0; i < numReq; i++ { go func() { res, err := c.Get(ts.URL) didReqCh <- true if err != nil { t.Errorf("client fetch error: %v", err) return } res.Body.Close() }() } // Wait for all goroutines to be stuck in the Handler. for i := 0; i < numReq; i++ { <-gotReqCh } nhigh := runtime.NumGoroutine() // Tell all handlers to unblock and reply. for i := 0; i < numReq; i++ { unblockCh <- true } // Wait for all HTTP clients to be done. for i := 0; i < numReq; i++ { <-didReqCh } tr.CloseIdleConnections() time.Sleep(100 * time.Millisecond) runtime.GC() runtime.GC() // even more. nfinal := runtime.NumGoroutine() growth := nfinal - n0 // We expect 0 or 1 extra goroutine, empirically. Allow up to 5. // Previously we were leaking one per numReq. if int(growth) > 5 { t.Logf("goroutine growth: %d -> %d -> %d (delta: %d)", n0, nhigh, nfinal, growth) t.Error("too many new goroutines") } } // golang.org/issue/4531: Transport leaks goroutines when // request.ContentLength is explicitly short func TestTransportPersistConnLeakShortBody(t *testing.T) { if runtime.GOOS == "plan9" { t.Skip("skipping test; see http://golang.org/issue/7237") } defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() tr := &Transport{} c := &Client{Transport: tr} n0 := runtime.NumGoroutine() body := []byte("Hello") for i := 0; i < 20; i++ { req, err := NewRequest("POST", ts.URL, bytes.NewReader(body)) if err != nil { t.Fatal(err) } req.ContentLength = int64(len(body) - 2) // explicitly short _, err = c.Do(req) if err == nil { t.Fatal("Expect an error from writing too long of a body.") } } nhigh := runtime.NumGoroutine() tr.CloseIdleConnections() time.Sleep(400 * time.Millisecond) runtime.GC() nfinal := runtime.NumGoroutine() growth := nfinal - n0 // We expect 0 or 1 extra goroutine, empirically. Allow up to 5. // Previously we were leaking one per numReq. t.Logf("goroutine growth: %d -> %d -> %d (delta: %d)", n0, nhigh, nfinal, growth) if int(growth) > 5 { t.Error("too many new goroutines") } } // This used to crash; http://golang.org/issue/3266 func TestTransportIdleConnCrash(t *testing.T) { defer afterTest(t) tr := &Transport{} c := &Client{Transport: tr} unblockCh := make(chan bool, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-unblockCh tr.CloseIdleConnections() })) defer ts.Close() didreq := make(chan bool) go func() { res, err := c.Get(ts.URL) if err != nil { t.Error(err) } else { res.Body.Close() // returns idle conn } didreq <- true }() unblockCh <- true <-didreq } // Test that the transport doesn't close the TCP connection early, // before the response body has been read. This was a regression // which sadly lacked a triggering test. The large response body made // the old race easier to trigger. func TestIssue3644(t *testing.T) { defer afterTest(t) const numFoos = 5000 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Connection", "close") for i := 0; i < numFoos; i++ { w.Write([]byte("foo ")) } })) defer ts.Close() tr := &Transport{} c := &Client{Transport: tr} res, err := c.Get(ts.URL) if err != nil { t.Fatal(err) } defer res.Body.Close() bs, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatal(err) } if len(bs) != numFoos*len("foo ") { t.Errorf("unexpected response length") } } // Test that a client receives a server's reply, even if the server doesn't read // the entire request body. func TestIssue3595(t *testing.T) { defer afterTest(t) const deniedMsg = "sorry, denied." ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, deniedMsg, http.StatusUnauthorized) })) defer ts.Close() tr := &Transport{} c := &Client{Transport: tr} res, err := c.Post(ts.URL, "application/octet-stream", neverEnding('a')) if err != nil { t.Errorf("Post: %v", err) return } got, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatalf("Body ReadAll: %v", err) } if !strings.Contains(string(got), deniedMsg) { t.Errorf("Known bug: response %q does not contain %q", got, deniedMsg) } } // From http://golang.org/issue/4454 , // "client fails to handle requests with no body and chunked encoding" func TestChunkedNoContent(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })) defer ts.Close() for _, closeBody := range []bool{true, false} { c := &Client{Transport: &Transport{}} const n = 4 for i := 1; i <= n; i++ { res, err := c.Get(ts.URL) if err != nil { t.Errorf("closingBody=%v, req %d/%d: %v", closeBody, i, n, err) } else { if closeBody { res.Body.Close() } } } } } func TestTransportConcurrency(t *testing.T) { defer afterTest(t) maxProcs, numReqs := 16, 500 if testing.Short() { maxProcs, numReqs = 4, 50 } defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(maxProcs)) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "%v", r.FormValue("echo")) })) defer ts.Close() var wg sync.WaitGroup wg.Add(numReqs) tr := &Transport{ Dial: func(netw, addr string) (c net.Conn, err error) { // Due to the Transport's "socket late // binding" (see idleConnCh in transport.go), // the numReqs HTTP requests below can finish // with a dial still outstanding. So count // our dials as work too so the leak checker // doesn't complain at us. wg.Add(1) defer wg.Done() return net.Dial(netw, addr) }, } defer tr.CloseIdleConnections() c := &Client{Transport: tr} reqs := make(chan string) defer close(reqs) for i := 0; i < maxProcs*2; i++ { go func() { for req := range reqs { res, err := c.Get(ts.URL + "/?echo=" + req) if err != nil { t.Errorf("error on req %s: %v", req, err) wg.Done() continue } all, err := ioutil.ReadAll(res.Body) if err != nil { t.Errorf("read error on req %s: %v", req, err) wg.Done() continue } if string(all) != req { t.Errorf("body of req %s = %q; want %q", req, all, req) } res.Body.Close() wg.Done() } }() } for i := 0; i < numReqs; i++ { reqs <- fmt.Sprintf("request-%d", i) } wg.Wait() } func TestIssue4191_InfiniteGetTimeout(t *testing.T) { if runtime.GOOS == "plan9" { t.Skip("skipping test; see http://golang.org/issue/7237") } defer afterTest(t) const debug = false mux := http.NewServeMux() mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { io.Copy(w, neverEnding('a')) }) ts := httptest.NewServer(mux) timeout := 100 * time.Millisecond client := &Client{ Transport: &Transport{ Dial: func(n, addr string) (net.Conn, error) { conn, err := net.Dial(n, addr) if err != nil { return nil, err } conn.SetDeadline(time.Now().Add(timeout)) if debug { conn = NewLoggingConn("client", conn) } return conn, nil }, DisableKeepAlives: true, }, } getFailed := false nRuns := 5 if testing.Short() { nRuns = 1 } for i := 0; i < nRuns; i++ { if debug { println("run", i+1, "of", nRuns) } sres, err := client.Get(ts.URL + "/get") if err != nil { if !getFailed { // Make the timeout longer, once. getFailed = true t.Logf("increasing timeout") i-- timeout *= 10 continue } t.Errorf("Error issuing GET: %v", err) break } _, err = io.Copy(ioutil.Discard, sres.Body) if err == nil { t.Errorf("Unexpected successful copy") break } } if debug { println("tests complete; waiting for handlers to finish") } ts.Close() } func TestIssue4191_InfiniteGetToPutTimeout(t *testing.T) { if runtime.GOOS == "plan9" { t.Skip("skipping test; see http://golang.org/issue/7237") } defer afterTest(t) const debug = false mux := http.NewServeMux() mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { io.Copy(w, neverEnding('a')) }) mux.HandleFunc("/put", func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() io.Copy(ioutil.Discard, r.Body) }) ts := httptest.NewServer(mux) timeout := 100 * time.Millisecond client := &Client{ Transport: &Transport{ Dial: func(n, addr string) (net.Conn, error) { conn, err := net.Dial(n, addr) if err != nil { return nil, err } conn.SetDeadline(time.Now().Add(timeout)) if debug { conn = NewLoggingConn("client", conn) } return conn, nil }, DisableKeepAlives: true, }, } getFailed := false nRuns := 5 if testing.Short() { nRuns = 1 } for i := 0; i < nRuns; i++ { if debug { println("run", i+1, "of", nRuns) } sres, err := client.Get(ts.URL + "/get") if err != nil { if !getFailed { // Make the timeout longer, once. getFailed = true t.Logf("increasing timeout") i-- timeout *= 10 continue } t.Errorf("Error issuing GET: %v", err) break } req, _ := NewRequest("PUT", ts.URL+"/put", sres.Body) _, err = client.Do(req) if err == nil { sres.Body.Close() t.Errorf("Unexpected successful PUT") break } sres.Body.Close() } if debug { println("tests complete; waiting for handlers to finish") } ts.Close() } func TestTransportResponseHeaderTimeout(t *testing.T) { defer afterTest(t) if testing.Short() { t.Skip("skipping timeout test in -short mode") } inHandler := make(chan bool, 1) mux := http.NewServeMux() mux.HandleFunc("/fast", func(w http.ResponseWriter, r *http.Request) { inHandler <- true }) mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) { inHandler <- true time.Sleep(2 * time.Second) }) ts := httptest.NewServer(mux) defer ts.Close() tr := &Transport{ ResponseHeaderTimeout: 500 * time.Millisecond, } defer tr.CloseIdleConnections() c := &Client{Transport: tr} tests := []struct { path string want int wantErr string }{ {path: "/fast", want: 200}, {path: "/slow", wantErr: "timeout awaiting response headers"}, {path: "/fast", want: 200}, } for i, tt := range tests { res, err := c.Get(ts.URL + tt.path) select { case <-inHandler: case <-time.After(5 * time.Second): t.Errorf("never entered handler for test index %d, %s", i, tt.path) continue } if err != nil { uerr, ok := err.(*url.Error) if !ok { t.Errorf("error is not an url.Error; got: %#v", err) continue } nerr, ok := uerr.Err.(net.Error) if !ok { t.Errorf("error does not satisfy net.Error interface; got: %#v", err) continue } if !nerr.Timeout() { t.Errorf("want timeout error; got: %q", nerr) continue } if strings.Contains(err.Error(), tt.wantErr) { continue } t.Errorf("%d. unexpected error: %v", i, err) continue } if tt.wantErr != "" { t.Errorf("%d. no error. expected error: %v", i, tt.wantErr) continue } if res.StatusCode != tt.want { t.Errorf("%d for path %q status = %d; want %d", i, tt.path, res.StatusCode, tt.want) } } } func TestTransportCancelRequest(t *testing.T) { defer afterTest(t) if testing.Short() { t.Skip("skipping test in -short mode") } unblockc := make(chan bool) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello") w.(http.Flusher).Flush() // send headers and some body <-unblockc })) defer ts.Close() defer close(unblockc) tr := &Transport{} defer tr.CloseIdleConnections() c := &Client{Transport: tr} req, _ := NewRequest("GET", ts.URL, nil) res, err := c.Do(req) if err != nil { t.Fatal(err) } go func() { time.Sleep(1 * time.Second) tr.CancelRequest(req) }() t0 := time.Now() body, err := ioutil.ReadAll(res.Body) d := time.Since(t0) if err == nil { t.Error("expected an error reading the body") } if string(body) != "Hello" { t.Errorf("Body = %q; want Hello", body) } if d < 500*time.Millisecond { t.Errorf("expected ~1 second delay; got %v", d) } // Verify no outstanding requests after readLoop/writeLoop // goroutines shut down. for tries := 3; tries > 0; tries-- { n := tr.NumPendingRequestsForTesting() if n == 0 { break } time.Sleep(100 * time.Millisecond) if tries == 1 { t.Errorf("pending requests = %d; want 0", n) } } } func TestTransportCancelRequestInDial(t *testing.T) { defer afterTest(t) if testing.Short() { t.Skip("skipping test in -short mode") } var logbuf bytes.Buffer eventLog := log.New(&logbuf, "", 0) unblockDial := make(chan bool) defer close(unblockDial) inDial := make(chan bool) tr := &Transport{ Dial: func(network, addr string) (net.Conn, error) { eventLog.Println("dial: blocking") inDial <- true <-unblockDial return nil, errors.New("nope") }, } cl := &Client{Transport: tr} gotres := make(chan bool) req, _ := NewRequest("GET", "http://something.no-network.tld/", nil) go func() { _, err := cl.Do(req) eventLog.Printf("Get = %v", err) gotres <- true }() select { case <-inDial: case <-time.After(5 * time.Second): t.Fatal("timeout; never saw blocking dial") } eventLog.Printf("canceling") tr.CancelRequest(req) select { case <-gotres: case <-time.After(5 * time.Second): panic("hang. events are: " + logbuf.String()) } got := logbuf.String() want := `dial: blocking canceling Get = Get http://something.no-network.tld/: net/http: request canceled while waiting for connection ` if got != want { t.Errorf("Got events:\n%s\nWant:\n%s", got, want) } } // golang.org/issue/3672 -- Client can't close HTTP stream // Calling Close on a Response.Body used to just read until EOF. // Now it actually closes the TCP connection. func TestTransportCloseResponseBody(t *testing.T) { defer afterTest(t) writeErr := make(chan error, 1) msg := []byte("young\n") ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for { _, err := w.Write(msg) if err != nil { writeErr <- err return } w.(http.Flusher).Flush() } })) defer ts.Close() tr := &Transport{} defer tr.CloseIdleConnections() c := &Client{Transport: tr} req, _ := NewRequest("GET", ts.URL, nil) defer tr.CancelRequest(req) res, err := c.Do(req) if err != nil { t.Fatal(err) } const repeats = 3 buf := make([]byte, len(msg)*repeats) want := bytes.Repeat(msg, repeats) _, err = io.ReadFull(res.Body, buf) if err != nil { t.Fatal(err) } if !bytes.Equal(buf, want) { t.Fatalf("read %q; want %q", buf, want) } didClose := make(chan error, 1) go func() { didClose <- res.Body.Close() }() select { case err := <-didClose: if err != nil { t.Errorf("Close = %v", err) } case <-time.After(10 * time.Second): t.Fatal("too long waiting for close") } select { case err := <-writeErr: if err == nil { t.Errorf("expected non-nil write error") } case <-time.After(10 * time.Second): t.Fatal("too long waiting for write error") } } type fooProto struct{} func (fooProto) RoundTrip(req *Request) (*Response, error) { res := &Response{ Status: "200 OK", StatusCode: 200, Header: make(http.Header), Body: ioutil.NopCloser(strings.NewReader("You wanted " + req.URL.String())), } return res, nil } func TestTransportAltProto(t *testing.T) { defer afterTest(t) tr := &Transport{} c := &Client{Transport: tr} tr.RegisterProtocol("foo", fooProto{}) res, err := c.Get("foo://bar.com/path") if err != nil { t.Fatal(err) } bodyb, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatal(err) } body := string(bodyb) if e := "You wanted foo://bar.com/path"; body != e { t.Errorf("got response %q, want %q", body, e) } } func TestTransportNoHost(t *testing.T) { defer afterTest(t) tr := &Transport{} _, err := tr.RoundTrip(&Request{ Header: make(http.Header), URL: &url.URL{ Scheme: "http", }, }) want := "http: no Host in request URL" if got := fmt.Sprint(err); got != want { t.Errorf("error = %v; want %q", err, want) } } func TestTransportSocketLateBinding(t *testing.T) { defer afterTest(t) mux := http.NewServeMux() fooGate := make(chan bool, 1) mux.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("foo-ipport", r.RemoteAddr) w.(http.Flusher).Flush() <-fooGate }) mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("bar-ipport", r.RemoteAddr) }) ts := httptest.NewServer(mux) defer ts.Close() dialGate := make(chan bool, 1) tr := &Transport{ Dial: func(n, addr string) (net.Conn, error) { if <-dialGate { return net.Dial(n, addr) } return nil, errors.New("manually closed") }, DisableKeepAlives: false, } defer tr.CloseIdleConnections() c := &Client{ Transport: tr, } dialGate <- true // only allow one dial fooRes, err := c.Get(ts.URL + "/foo") if err != nil { t.Fatal(err) } fooAddr := fooRes.Header.Get("foo-ipport") if fooAddr == "" { t.Fatal("No addr on /foo request") } time.AfterFunc(200*time.Millisecond, func() { // let the foo response finish so we can use its // connection for /bar fooGate <- true io.Copy(ioutil.Discard, fooRes.Body) fooRes.Body.Close() }) barRes, err := c.Get(ts.URL + "/bar") if err != nil { t.Fatal(err) } barAddr := barRes.Header.Get("bar-ipport") if barAddr != fooAddr { t.Fatalf("/foo came from conn %q; /bar came from %q instead", fooAddr, barAddr) } barRes.Body.Close() dialGate <- false } // Issue 2184 func TestTransportReading100Continue(t *testing.T) { defer afterTest(t) const numReqs = 5 reqBody := func(n int) string { return fmt.Sprintf("request body %d", n) } reqID := func(n int) string { return fmt.Sprintf("REQ-ID-%d", n) } send100Response := func(w *io.PipeWriter, r *io.PipeReader) { defer w.Close() defer r.Close() br := bufio.NewReader(r) n := 0 for { n++ req, err := ReadRequest(br) if err == io.EOF { return } if err != nil { t.Error(err) return } slurp, err := ioutil.ReadAll(req.Body) if err != nil { t.Errorf("Server request body slurp: %v", err) return } id := req.Header.Get("Request-Id") resCode := req.Header.Get("X-Want-Response-Code") if resCode == "" { resCode = "100 Continue" if string(slurp) != reqBody(n) { t.Errorf("Server got %q, %v; want %q", slurp, err, reqBody(n)) } } body := fmt.Sprintf("Response number %d", n) v := []byte(strings.Replace(fmt.Sprintf(`HTTP/1.1 %s Date: Thu, 28 Feb 2013 17:55:41 GMT HTTP/1.1 200 OK Content-Type: text/html Echo-Request-Id: %s Content-Length: %d %s`, resCode, id, len(body), body), "\n", "\r\n", -1)) w.Write(v) if id == reqID(numReqs) { return } } } tr := &Transport{ Dial: func(n, addr string) (net.Conn, error) { sr, sw := io.Pipe() // server read/write cr, cw := io.Pipe() // client read/write conn := &rwTestConn{ Reader: cr, Writer: sw, closeFunc: func() error { sw.Close() cw.Close() return nil }, } go send100Response(cw, sr) return conn, nil }, DisableKeepAlives: false, } defer tr.CloseIdleConnections() c := &Client{Transport: tr} testResponse := func(req *Request, name string, wantCode int) { res, err := c.Do(req) if err != nil { t.Fatalf("%s: Do: %v", name, err) } if res.StatusCode != wantCode { t.Fatalf("%s: Response Statuscode=%d; want %d", name, res.StatusCode, wantCode) } if id, idBack := req.Header.Get("Request-Id"), res.Header.Get("Echo-Request-Id"); id != "" && id != idBack { t.Errorf("%s: response id %q != request id %q", name, idBack, id) } _, err = ioutil.ReadAll(res.Body) if err != nil { t.Fatalf("%s: Slurp error: %v", name, err) } } // Few 100 responses, making sure we're not off-by-one. for i := 1; i <= numReqs; i++ { req, _ := NewRequest("POST", "http://dummy.tld/", strings.NewReader(reqBody(i))) req.Header.Set("Request-Id", reqID(i)) testResponse(req, fmt.Sprintf("100, %d/%d", i, numReqs), 200) } // And some other informational 1xx but non-100 responses, to test // we return them but don't re-use the connection. for i := 1; i <= numReqs; i++ { req, _ := NewRequest("POST", "http://other.tld/", strings.NewReader(reqBody(i))) req.Header.Set("X-Want-Response-Code", "123 Sesame Street") testResponse(req, fmt.Sprintf("123, %d/%d", i, numReqs), 123) } } type proxyFromEnvTest struct { req string // URL to fetch; blank means "http://example.com" env string noenv string want string wanterr error } func (t proxyFromEnvTest) String() string { var buf bytes.Buffer if t.env != "" { fmt.Fprintf(&buf, "http_proxy=%q", t.env) } if t.noenv != "" { fmt.Fprintf(&buf, " no_proxy=%q", t.noenv) } req := "http://example.com" if t.req != "" { req = t.req } fmt.Fprintf(&buf, " req=%q", req) return strings.TrimSpace(buf.String()) } var proxyFromEnvTests = []proxyFromEnvTest{ {env: "127.0.0.1:8080", want: "http://127.0.0.1:8080"}, {env: "cache.corp.example.com:1234", want: "http://cache.corp.example.com:1234"}, {env: "cache.corp.example.com", want: "http://cache.corp.example.com"}, {env: "https://cache.corp.example.com", want: "https://cache.corp.example.com"}, {env: "http://127.0.0.1:8080", want: "http://127.0.0.1:8080"}, {env: "https://127.0.0.1:8080", want: "https://127.0.0.1:8080"}, {want: ""}, {noenv: "example.com", req: "http://example.com/", env: "proxy", want: ""}, {noenv: ".example.com", req: "http://example.com/", env: "proxy", want: ""}, {noenv: "ample.com", req: "http://example.com/", env: "proxy", want: "http://proxy"}, {noenv: "example.com", req: "http://foo.example.com/", env: "proxy", want: ""}, {noenv: ".foo.com", req: "http://example.com/", env: "proxy", want: "http://proxy"}, } func TestProxyFromEnvironment(t *testing.T) { ResetProxyEnv() for _, tt := range proxyFromEnvTests { os.Setenv("HTTP_PROXY", tt.env) os.Setenv("NO_PROXY", tt.noenv) ResetCachedEnvironment() reqURL := tt.req if reqURL == "" { reqURL = "http://example.com" } req, _ := NewRequest("GET", reqURL, nil) url, err := ProxyFromEnvironment(req) if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.wanterr); g != e { t.Errorf("%v: got error = %q, want %q", tt, g, e) continue } if got := fmt.Sprintf("%s", url); got != tt.want { t.Errorf("%v: got URL = %q, want %q", tt, url, tt.want) } } } func TestIdleConnChannelLeak(t *testing.T) { var mu sync.Mutex var n int ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() n++ mu.Unlock() })) defer ts.Close() tr := &Transport{ Dial: func(netw, addr string) (net.Conn, error) { return net.Dial(netw, ts.Listener.Addr().String()) }, } defer tr.CloseIdleConnections() c := &Client{Transport: tr} // First, without keep-alives. for _, disableKeep := range []bool{true, false} { tr.DisableKeepAlives = disableKeep for i := 0; i < 5; i++ { _, err := c.Get(fmt.Sprintf("http://foo-host-%d.tld/", i)) if err != nil { t.Fatal(err) } } if got := tr.IdleConnChMapSizeForTesting(); got != 0 { t.Fatalf("ForDisableKeepAlives = %v, map size = %d; want 0", disableKeep, got) } } } // Verify the status quo: that the Client.Post function coerces its // body into a ReadCloser if it's a Closer, and that the Transport // then closes it. func TestTransportClosesRequestBody(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.Copy(ioutil.Discard, r.Body) })) defer ts.Close() tr := &Transport{} defer tr.CloseIdleConnections() cl := &Client{Transport: tr} closes := 0 res, err := cl.Post(ts.URL, "text/plain", countCloseReader{&closes, strings.NewReader("hello")}) if err != nil { t.Fatal(err) } res.Body.Close() if closes != 1 { t.Errorf("closes = %d; want 1", closes) } } func TestTransportTLSHandshakeTimeout(t *testing.T) { defer afterTest(t) if testing.Short() { t.Skip("skipping in short mode") } ln := newLocalListener(t) defer ln.Close() testdonec := make(chan struct{}) defer close(testdonec) go func() { c, err := ln.Accept() if err != nil { t.Error(err) return } <-testdonec c.Close() }() getdonec := make(chan struct{}) go func() { defer close(getdonec) tr := &Transport{ Dial: func(_, _ string) (net.Conn, error) { return net.Dial("tcp", ln.Addr().String()) }, TLSHandshakeTimeout: 250 * time.Millisecond, } cl := &Client{Transport: tr} _, err := cl.Get("https://dummy.tld/") if err == nil { t.Error("expected error") return } ue, ok := err.(*url.Error) if !ok { t.Errorf("expected url.Error; got %#v", err) return } ne, ok := ue.Err.(net.Error) if !ok { t.Errorf("expected net.Error; got %#v", err) return } if !ne.Timeout() { t.Errorf("expected timeout error; got %v", err) } if !strings.Contains(err.Error(), "handshake timeout") { t.Errorf("expected 'handshake timeout' in error; got %v", err) } }() select { case <-getdonec: case <-time.After(5 * time.Second): t.Error("test timeout; TLS handshake hung?") } } // Trying to repro golang.org/issue/3514 func TestTLSServerClosesConnection(t *testing.T) { defer afterTest(t) if runtime.GOOS == "windows" { t.Skip("skipping flaky test on Windows; golang.org/issue/7634") } closedc := make(chan bool, 1) ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/keep-alive-then-die") { conn, _, _ := w.(http.Hijacker).Hijack() conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nfoo")) conn.Close() closedc <- true return } fmt.Fprintf(w, "hello") })) defer ts.Close() tr := &Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, } defer tr.CloseIdleConnections() client := &Client{Transport: tr} var nSuccess = 0 var errs []error const trials = 20 for i := 0; i < trials; i++ { tr.CloseIdleConnections() res, err := client.Get(ts.URL + "/keep-alive-then-die") if err != nil { t.Fatal(err) } <-closedc slurp, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatal(err) } if string(slurp) != "foo" { t.Errorf("Got %q, want foo", slurp) } // Now try again and see if we successfully // pick a new connection. res, err = client.Get(ts.URL + "/") if err != nil { errs = append(errs, err) continue } slurp, err = ioutil.ReadAll(res.Body) if err != nil { errs = append(errs, err) continue } nSuccess++ } if nSuccess > 0 { t.Logf("successes = %d of %d", nSuccess, trials) } else { t.Errorf("All runs failed:") } for _, err := range errs { t.Logf(" err: %v", err) } } // byteFromChanReader is an io.Reader that reads a single byte at a // time from the channel. When the channel is closed, the reader // returns io.EOF. type byteFromChanReader chan byte func (c byteFromChanReader) Read(p []byte) (n int, err error) { if len(p) == 0 { return } b, ok := <-c if !ok { return 0, io.EOF } p[0] = b return 1, nil } // Verifies that the Transport doesn't reuse a connection in the case // where the server replies before the request has been fully // written. We still honor that reply (see TestIssue3595), but don't // send future requests on the connection because it's then in a // questionable state. // golang.org/issue/7569 func TestTransportNoReuseAfterEarlyResponse(t *testing.T) { defer afterTest(t) var sconn struct { sync.Mutex c net.Conn } var getOkay bool closeConn := func() { sconn.Lock() defer sconn.Unlock() if sconn.c != nil { sconn.c.Close() sconn.c = nil if !getOkay { t.Logf("Closed server connection") } } } defer closeConn() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { io.WriteString(w, "bar") return } conn, _, _ := w.(http.Hijacker).Hijack() sconn.Lock() sconn.c = conn sconn.Unlock() conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nfoo")) // keep-alive go io.Copy(ioutil.Discard, conn) })) defer ts.Close() tr := &Transport{} defer tr.CloseIdleConnections() client := &Client{Transport: tr} const bodySize = 256 << 10 finalBit := make(byteFromChanReader, 1) req, _ := NewRequest("POST", ts.URL, io.MultiReader(io.LimitReader(neverEnding('x'), bodySize-1), finalBit)) req.ContentLength = bodySize res, err := client.Do(req) if err := wantBody(res, err, "foo"); err != nil { t.Errorf("POST response: %v", err) } donec := make(chan bool) go func() { defer close(donec) res, err = client.Get(ts.URL) if err := wantBody(res, err, "bar"); err != nil { t.Errorf("GET response: %v", err) return } getOkay = true // suppress test noise }() time.AfterFunc(5*time.Second, closeConn) select { case <-donec: finalBit <- 'x' // unblock the writeloop of the first Post close(finalBit) case <-time.After(7 * time.Second): t.Fatal("timeout waiting for GET request to finish") } } type errorReader struct { err error } func (e errorReader) Read(p []byte) (int, error) { return 0, e.err } type closerFunc func() error func (f closerFunc) Close() error { return f() } // Issue 6981 func TestTransportClosesBodyOnError(t *testing.T) { if runtime.GOOS == "plan9" { t.Skip("skipping test; see http://golang.org/issue/7782") } defer afterTest(t) readBody := make(chan error, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := ioutil.ReadAll(r.Body) readBody <- err })) defer ts.Close() fakeErr := errors.New("fake error") didClose := make(chan bool, 1) req, _ := NewRequest("POST", ts.URL, struct { io.Reader io.Closer }{ io.MultiReader(io.LimitReader(neverEnding('x'), 1<<20), errorReader{fakeErr}), closerFunc(func() error { select { case didClose <- true: default: } return nil }), }) res, err := DefaultClient.Do(req) if res != nil { defer res.Body.Close() } if err == nil || !strings.Contains(err.Error(), fakeErr.Error()) { t.Fatalf("Do error = %v; want something containing %q", err, fakeErr.Error()) } select { case err := <-readBody: if err == nil { t.Errorf("Unexpected success reading request body from handler; want 'unexpected EOF reading trailer'") } case <-time.After(5 * time.Second): t.Error("timeout waiting for server handler to complete") } select { case <-didClose: default: t.Errorf("didn't see Body.Close") } } func wantBody(res *Response, err error, want string) error { if err != nil { return err } slurp, err := ioutil.ReadAll(res.Body) if err != nil { return fmt.Errorf("error reading body: %v", err) } if string(slurp) != want { return fmt.Errorf("body = %q; want %q", slurp, want) } if err := res.Body.Close(); err != nil { return fmt.Errorf("body Close = %v", err) } return nil } func newLocalListener(t *testing.T) net.Listener { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { ln, err = net.Listen("tcp6", "[::1]:0") } if err != nil { t.Fatal(err) } return ln } type countCloseReader struct { n *int io.Reader } func (cr countCloseReader) Close() error { (*cr.n)++ return nil } // rgz is a gzip quine that uncompresses to itself. var rgz = []byte{ 0x1f, 0x8b, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x00, 0x92, 0xef, 0xe6, 0xe0, 0x60, 0x00, 0x83, 0xa2, 0xd4, 0xe4, 0xd2, 0xa2, 0xe2, 0xcc, 0xb2, 0x54, 0x06, 0x00, 0x00, 0x17, 0x00, 0xe8, 0xff, 0x92, 0xef, 0xe6, 0xe0, 0x60, 0x00, 0x83, 0xa2, 0xd4, 0xe4, 0xd2, 0xa2, 0xe2, 0xcc, 0xb2, 0x54, 0x06, 0x00, 0x00, 0x17, 0x00, 0xe8, 0xff, 0x42, 0x12, 0x46, 0x16, 0x06, 0x00, 0x05, 0x00, 0xfa, 0xff, 0x42, 0x12, 0x46, 0x16, 0x06, 0x00, 0x05, 0x00, 0xfa, 0xff, 0x00, 0x05, 0x00, 0xfa, 0xff, 0x00, 0x14, 0x00, 0xeb, 0xff, 0x42, 0x12, 0x46, 0x16, 0x06, 0x00, 0x05, 0x00, 0xfa, 0xff, 0x00, 0x05, 0x00, 0xfa, 0xff, 0x00, 0x14, 0x00, 0xeb, 0xff, 0x42, 0x88, 0x21, 0xc4, 0x00, 0x00, 0x14, 0x00, 0xeb, 0xff, 0x42, 0x88, 0x21, 0xc4, 0x00, 0x00, 0x14, 0x00, 0xeb, 0xff, 0x42, 0x88, 0x21, 0xc4, 0x00, 0x00, 0x14, 0x00, 0xeb, 0xff, 0x42, 0x88, 0x21, 0xc4, 0x00, 0x00, 0x14, 0x00, 0xeb, 0xff, 0x42, 0x88, 0x21, 0xc4, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x17, 0x00, 0xe8, 0xff, 0x42, 0x88, 0x21, 0xc4, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x17, 0x00, 0xe8, 0xff, 0x42, 0x12, 0x46, 0x16, 0x06, 0x00, 0x00, 0x00, 0xff, 0xff, 0x01, 0x08, 0x00, 0xf7, 0xff, 0x3d, 0xb1, 0x20, 0x85, 0xfa, 0x00, 0x00, 0x00, 0x42, 0x12, 0x46, 0x16, 0x06, 0x00, 0x00, 0x00, 0xff, 0xff, 0x01, 0x08, 0x00, 0xf7, 0xff, 0x3d, 0xb1, 0x20, 0x85, 0xfa, 0x00, 0x00, 0x00, 0x3d, 0xb1, 0x20, 0x85, 0xfa, 0x00, 0x00, 0x00, } ubuntu-push-0.68+16.04.20160310.2/http13client/requestwrite_test.go0000644000015600001650000003333612670364255025140 0ustar pbuserpbgroup00000000000000// Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "bytes" "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" "strings" "testing" ) type reqWriteTest struct { Req Request Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body // Any of these three may be empty to skip that test. WantWrite string // Request.Write WantProxy string // Request.WriteProxy WantError error // wanted error from Request.Write } var reqWriteTests = []reqWriteTest{ // HTTP/1.1 => chunked coding; no body; no trailer { Req: Request{ Method: "GET", URL: &url.URL{ Scheme: "http", Host: "www.techcrunch.com", Path: "/", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, "Accept-Charset": {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"}, "Accept-Encoding": {"gzip,deflate"}, "Accept-Language": {"en-us,en;q=0.5"}, "Keep-Alive": {"300"}, "Proxy-Connection": {"keep-alive"}, "User-Agent": {"Fake"}, }, Body: nil, Close: false, Host: "www.techcrunch.com", Form: map[string][]string{}, }, WantWrite: "GET / HTTP/1.1\r\n" + "Host: www.techcrunch.com\r\n" + "User-Agent: Fake\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + "Accept-Encoding: gzip,deflate\r\n" + "Accept-Language: en-us,en;q=0.5\r\n" + "Keep-Alive: 300\r\n" + "Proxy-Connection: keep-alive\r\n\r\n", WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" + "Host: www.techcrunch.com\r\n" + "User-Agent: Fake\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + "Accept-Encoding: gzip,deflate\r\n" + "Accept-Language: en-us,en;q=0.5\r\n" + "Keep-Alive: 300\r\n" + "Proxy-Connection: keep-alive\r\n\r\n", }, // HTTP/1.1 => chunked coding; body; empty trailer { Req: Request{ Method: "GET", URL: &url.URL{ Scheme: "http", Host: "www.google.com", Path: "/search", }, ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, TransferEncoding: []string{"chunked"}, }, Body: []byte("abcdef"), WantWrite: "GET /search HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + chunk("abcdef") + chunk(""), WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + chunk("abcdef") + chunk(""), }, // HTTP/1.1 POST => chunked coding; body; empty trailer { Req: Request{ Method: "POST", URL: &url.URL{ Scheme: "http", Host: "www.google.com", Path: "/search", }, ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, Close: true, TransferEncoding: []string{"chunked"}, }, Body: []byte("abcdef"), WantWrite: "POST /search HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Connection: close\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + chunk("abcdef") + chunk(""), WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Connection: close\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + chunk("abcdef") + chunk(""), }, // HTTP/1.1 POST with Content-Length, no chunking { Req: Request{ Method: "POST", URL: &url.URL{ Scheme: "http", Host: "www.google.com", Path: "/search", }, ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, Close: true, ContentLength: 6, }, Body: []byte("abcdef"), WantWrite: "POST /search HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Connection: close\r\n" + "Content-Length: 6\r\n" + "\r\n" + "abcdef", WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Connection: close\r\n" + "Content-Length: 6\r\n" + "\r\n" + "abcdef", }, // HTTP/1.1 POST with Content-Length in headers { Req: Request{ Method: "POST", URL: mustParseURL("http://example.com/"), Host: "example.com", Header: http.Header{ "Content-Length": []string{"10"}, // ignored }, ContentLength: 6, }, Body: []byte("abcdef"), WantWrite: "POST / HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Content-Length: 6\r\n" + "\r\n" + "abcdef", WantProxy: "POST http://example.com/ HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Content-Length: 6\r\n" + "\r\n" + "abcdef", }, // default to HTTP/1.1 { Req: Request{ Method: "GET", URL: mustParseURL("/search"), Host: "www.google.com", }, WantWrite: "GET /search HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "\r\n", }, // Request with a 0 ContentLength and a 0 byte body. { Req: Request{ Method: "POST", URL: mustParseURL("/"), Host: "example.com", ProtoMajor: 1, ProtoMinor: 1, ContentLength: 0, // as if unset by user }, Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) }, // RFC 2616 Section 14.13 says Content-Length should be specified // unless body is prohibited by the request method. // Also, nginx expects it for POST and PUT. WantWrite: "POST / HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Content-Length: 0\r\n" + "\r\n", WantProxy: "POST / HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Content-Length: 0\r\n" + "\r\n", }, // Request with a 0 ContentLength and a 1 byte body. { Req: Request{ Method: "POST", URL: mustParseURL("/"), Host: "example.com", ProtoMajor: 1, ProtoMinor: 1, ContentLength: 0, // as if unset by user }, Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) }, WantWrite: "POST / HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + chunk("x") + chunk(""), WantProxy: "POST / HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + chunk("x") + chunk(""), }, // Request with a ContentLength of 10 but a 5 byte body. { Req: Request{ Method: "POST", URL: mustParseURL("/"), Host: "example.com", ProtoMajor: 1, ProtoMinor: 1, ContentLength: 10, // but we're going to send only 5 bytes }, Body: []byte("12345"), WantError: errors.New("http: Request.ContentLength=10 with Body length 5"), }, // Request with a ContentLength of 4 but an 8 byte body. { Req: Request{ Method: "POST", URL: mustParseURL("/"), Host: "example.com", ProtoMajor: 1, ProtoMinor: 1, ContentLength: 4, // but we're going to try to send 8 bytes }, Body: []byte("12345678"), WantError: errors.New("http: Request.ContentLength=4 with Body length 8"), }, // Request with a 5 ContentLength and nil body. { Req: Request{ Method: "POST", URL: mustParseURL("/"), Host: "example.com", ProtoMajor: 1, ProtoMinor: 1, ContentLength: 5, // but we'll omit the body }, WantError: errors.New("http: Request.ContentLength=5 with nil Body"), }, // Request with a 0 ContentLength and a body with 1 byte content and an error. { Req: Request{ Method: "POST", URL: mustParseURL("/"), Host: "example.com", ProtoMajor: 1, ProtoMinor: 1, ContentLength: 0, // as if unset by user }, Body: func() io.ReadCloser { err := errors.New("Custom reader error") errReader := &errorReader{err} return ioutil.NopCloser(io.MultiReader(strings.NewReader("x"), errReader)) }, WantError: errors.New("Custom reader error"), }, // Request with a 0 ContentLength and a body without content and an error. { Req: Request{ Method: "POST", URL: mustParseURL("/"), Host: "example.com", ProtoMajor: 1, ProtoMinor: 1, ContentLength: 0, // as if unset by user }, Body: func() io.ReadCloser { err := errors.New("Custom reader error") errReader := &errorReader{err} return ioutil.NopCloser(errReader) }, WantError: errors.New("Custom reader error"), }, // Verify that DumpRequest preserves the HTTP version number, doesn't add a Host, // and doesn't add a User-Agent. { Req: Request{ Method: "GET", URL: mustParseURL("/foo"), ProtoMajor: 1, ProtoMinor: 0, Header: http.Header{ "X-Foo": []string{"X-Bar"}, }, }, WantWrite: "GET /foo HTTP/1.1\r\n" + "Host: \r\n" + "User-Agent: Go 1.1 package http\r\n" + "X-Foo: X-Bar\r\n\r\n", }, // If no Request.Host and no Request.URL.Host, we send // an empty Host header, and don't use // Request.Header["Host"]. This is just testing that // we don't change Go 1.0 behavior. { Req: Request{ Method: "GET", Host: "", URL: &url.URL{ Scheme: "http", Host: "", Path: "/search", }, ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "Host": []string{"bad.example.com"}, }, }, WantWrite: "GET /search HTTP/1.1\r\n" + "Host: \r\n" + "User-Agent: Go 1.1 package http\r\n\r\n", }, // Opaque test #1 from golang.org/issue/4860 { Req: Request{ Method: "GET", URL: &url.URL{ Scheme: "http", Host: "www.google.com", Opaque: "/%2F/%2F/", }, ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, }, WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "User-Agent: Go 1.1 package http\r\n\r\n", }, // Opaque test #2 from golang.org/issue/4860 { Req: Request{ Method: "GET", URL: &url.URL{ Scheme: "http", Host: "x.google.com", Opaque: "//y.google.com/%2F/%2F/", }, ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, }, WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" + "Host: x.google.com\r\n" + "User-Agent: Go 1.1 package http\r\n\r\n", }, // Testing custom case in header keys. Issue 5022. { Req: Request{ Method: "GET", URL: &url.URL{ Scheme: "http", Host: "www.google.com", Path: "/", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "ALL-CAPS": {"x"}, }, }, WantWrite: "GET / HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "ALL-CAPS: x\r\n" + "\r\n", }, } func TestRequestWrite(t *testing.T) { for i := range reqWriteTests { tt := &reqWriteTests[i] setBody := func() { if tt.Body == nil { return } switch b := tt.Body.(type) { case []byte: tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b)) case func() io.ReadCloser: tt.Req.Body = b() } } setBody() if tt.Req.Header == nil { tt.Req.Header = make(http.Header) } var braw bytes.Buffer err := tt.Req.Write(&braw) if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e { t.Errorf("writing #%d, err = %q, want %q", i, g, e) continue } if err != nil { continue } if tt.WantWrite != "" { sraw := braw.String() if sraw != tt.WantWrite { t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw) continue } } if tt.WantProxy != "" { setBody() var praw bytes.Buffer err = tt.Req.WriteProxy(&praw) if err != nil { t.Errorf("WriteProxy #%d: %s", i, err) continue } sraw := praw.String() if sraw != tt.WantProxy { t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw) continue } } } } type closeChecker struct { io.Reader closed bool } func (rc *closeChecker) Close() error { rc.closed = true return nil } // TestRequestWriteClosesBody tests that Request.Write does close its request.Body. // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer // inside a NopCloser, and that it serializes it correctly. func TestRequestWriteClosesBody(t *testing.T) { rc := &closeChecker{Reader: strings.NewReader("my body")} req, _ := NewRequest("POST", "http://foo.com/", rc) if req.ContentLength != 0 { t.Errorf("got req.ContentLength %d, want 0", req.ContentLength) } buf := new(bytes.Buffer) req.Write(buf) if !rc.closed { t.Error("body not closed after write") } expected := "POST / HTTP/1.1\r\n" + "Host: foo.com\r\n" + "User-Agent: Go 1.1 package http\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + // TODO: currently we don't buffer before chunking, so we get a // single "m" chunk before the other chunks, as this was the 1-byte // read from our MultiReader where we stiched the Body back together // after sniffing whether the Body was 0 bytes or not. chunk("m") + chunk("y body") + chunk("") if buf.String() != expected { t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected) } } func chunk(s string) string { return fmt.Sprintf("%x\r\n%s\r\n", len(s), s) } func mustParseURL(s string) *url.URL { u, err := url.Parse(s) if err != nil { panic(fmt.Sprintf("Error parsing URL %q: %v", s, err)) } return u } ubuntu-push-0.68+16.04.20160310.2/http13client/request.go0000644000015600001650000006263412670364255023031 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // HTTP Request reading and parsing. package http import ( "bufio" "bytes" "crypto/tls" "errors" "fmt" "io" "io/ioutil" "mime" "mime/multipart" "net/http" "net/textproto" "net/url" "strconv" "strings" ) const ( maxValueLength = 4096 maxHeaderLines = 1024 chunkSize = 4 << 10 // 4 KB chunks defaultMaxMemory = 32 << 20 // 32 MB ) // ErrMissingFile is returned by FormFile when the provided file field name // is either not present in the request or not a file field. var ErrMissingFile = errors.New("http: no such file") // HTTP request parsing errors. type ProtocolError struct { ErrorString string } func (err *ProtocolError) Error() string { return err.ErrorString } var ( ErrHeaderTooLong = &ProtocolError{"header too long"} ErrShortBody = &ProtocolError{"entity body too short"} ErrNotSupported = &ProtocolError{"feature not supported"} ErrUnexpectedTrailer = &ProtocolError{"trailer header without chunked transfer encoding"} ErrMissingContentLength = &ProtocolError{"missing ContentLength in HEAD response"} ErrNotMultipart = &ProtocolError{"request Content-Type isn't multipart/form-data"} ErrMissingBoundary = &ProtocolError{"no multipart boundary param in Content-Type"} ) type badStringError struct { what string str string } func (e *badStringError) Error() string { return fmt.Sprintf("%s %q", e.what, e.str) } // Headers that Request.Write handles itself and should be skipped. var reqWriteExcludeHeader = map[string]bool{ "Host": true, // not in Header map anyway "User-Agent": true, "Content-Length": true, "Transfer-Encoding": true, "Trailer": true, } // A Request represents an HTTP request received by a server // or to be sent by a client. // // The field semantics differ slightly between client and server // usage. In addition to the notes on the fields below, see the // documentation for Request.Write and RoundTripper. type Request struct { // Method specifies the HTTP method (GET, POST, PUT, etc.). // For client requests an empty string means GET. Method string // URL specifies either the URI being requested (for server // requests) or the URL to access (for client requests). // // For server requests the URL is parsed from the URI // supplied on the Request-Line as stored in RequestURI. For // most requests, fields other than Path and RawQuery will be // empty. (See RFC 2616, Section 5.1.2) // // For client requests, the URL's Host specifies the server to // connect to, while the Request's Host field optionally // specifies the Host header value to send in the HTTP // request. URL *url.URL // The protocol version for incoming requests. // Client requests always use HTTP/1.1. Proto string // "HTTP/1.0" ProtoMajor int // 1 ProtoMinor int // 0 // A header maps request lines to their values. // If the header says // // accept-encoding: gzip, deflate // Accept-Language: en-us // Connection: keep-alive // // then // // Header = map[string][]string{ // "Accept-Encoding": {"gzip, deflate"}, // "Accept-Language": {"en-us"}, // "Connection": {"keep-alive"}, // } // // HTTP defines that header names are case-insensitive. // The request parser implements this by canonicalizing the // name, making the first character and any characters // following a hyphen uppercase and the rest lowercase. // // For client requests certain headers are automatically // added and may override values in Header. // // See the documentation for the Request.Write method. Header http.Header // Body is the request's body. // // For client requests a nil body means the request has no // body, such as a GET request. The HTTP Client's Transport // is responsible for calling the Close method. // // For server requests the Request Body is always non-nil // but will return EOF immediately when no body is present. // The Server will close the request body. The ServeHTTP // Handler does not need to. Body io.ReadCloser // ContentLength records the length of the associated content. // The value -1 indicates that the length is unknown. // Values >= 0 indicate that the given number of bytes may // be read from Body. // For client requests, a value of 0 means unknown if Body is not nil. ContentLength int64 // TransferEncoding lists the transfer encodings from outermost to // innermost. An empty list denotes the "identity" encoding. // TransferEncoding can usually be ignored; chunked encoding is // automatically added and removed as necessary when sending and // receiving requests. TransferEncoding []string // Close indicates whether to close the connection after // replying to this request (for servers) or after sending // the request (for clients). Close bool // For server requests Host specifies the host on which the // URL is sought. Per RFC 2616, this is either the value of // the "Host" header or the host name given in the URL itself. // It may be of the form "host:port". // // For client requests Host optionally overrides the Host // header to send. If empty, the Request.Write method uses // the value of URL.Host. Host string // Form contains the parsed form data, including both the URL // field's query parameters and the POST or PUT form data. // This field is only available after ParseForm is called. // The HTTP client ignores Form and uses Body instead. Form url.Values // PostForm contains the parsed form data from POST or PUT // body parameters. // This field is only available after ParseForm is called. // The HTTP client ignores PostForm and uses Body instead. PostForm url.Values // MultipartForm is the parsed multipart form, including file uploads. // This field is only available after ParseMultipartForm is called. // The HTTP client ignores MultipartForm and uses Body instead. MultipartForm *multipart.Form // Trailer specifies additional headers that are sent after the request // body. // // For server requests the Trailer map initially contains only the // trailer keys, with nil values. (The client declares which trailers it // will later send.) While the handler is reading from Body, it must // not reference Trailer. After reading from Body returns EOF, Trailer // can be read again and will contain non-nil values, if they were sent // by the client. // // For client requests Trailer must be initialized to a map containing // the trailer keys to later send. The values may be nil or their final // values. The ContentLength must be 0 or -1, to send a chunked request. // After the HTTP request is sent the map values can be updated while // the request body is read. Once the body returns EOF, the caller must // not mutate Trailer. // // Few HTTP clients, servers, or proxies support HTTP trailers. Trailer http.Header // RemoteAddr allows HTTP servers and other software to record // the network address that sent the request, usually for // logging. This field is not filled in by ReadRequest and // has no defined format. The HTTP server in this package // sets RemoteAddr to an "IP:port" address before invoking a // handler. // This field is ignored by the HTTP client. RemoteAddr string // RequestURI is the unmodified Request-URI of the // Request-Line (RFC 2616, Section 5.1) as sent by the client // to a server. Usually the URL field should be used instead. // It is an error to set this field in an HTTP client request. RequestURI string // TLS allows HTTP servers and other software to record // information about the TLS connection on which the request // was received. This field is not filled in by ReadRequest. // The HTTP server in this package sets the field for // TLS-enabled connections before invoking a handler; // otherwise it leaves the field nil. // This field is ignored by the HTTP client. TLS *tls.ConnectionState } // ProtoAtLeast reports whether the HTTP protocol used // in the request is at least major.minor. func (r *Request) ProtoAtLeast(major, minor int) bool { return r.ProtoMajor > major || r.ProtoMajor == major && r.ProtoMinor >= minor } // UserAgent returns the client's User-Agent, if sent in the request. func (r *Request) UserAgent() string { return r.Header.Get("User-Agent") } // Cookies parses and returns the HTTP cookies sent with the request. func (r *Request) Cookies() []*http.Cookie { return readCookies(r.Header, "") } var ErrNoCookie = errors.New("http: named cookie not present") // Cookie returns the named cookie provided in the request or // ErrNoCookie if not found. func (r *Request) Cookie(name string) (*http.Cookie, error) { for _, c := range readCookies(r.Header, name) { return c, nil } return nil, ErrNoCookie } // AddCookie adds a cookie to the request. Per RFC 6265 section 5.4, // AddCookie does not attach more than one Cookie header field. That // means all cookies, if any, are written into the same line, // separated by semicolon. func (r *Request) AddCookie(c *http.Cookie) { s := fmt.Sprintf("%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value)) if c := r.Header.Get("Cookie"); c != "" { r.Header.Set("Cookie", c+"; "+s) } else { r.Header.Set("Cookie", s) } } // Referer returns the referring URL, if sent in the request. // // Referer is misspelled as in the request itself, a mistake from the // earliest days of HTTP. This value can also be fetched from the // Header map as Header["Referer"]; the benefit of making it available // as a method is that the compiler can diagnose programs that use the // alternate (correct English) spelling req.Referrer() but cannot // diagnose programs that use Header["Referrer"]. func (r *Request) Referer() string { return r.Header.Get("Referer") } // multipartByReader is a sentinel value. // Its presence in Request.MultipartForm indicates that parsing of the request // body has been handed off to a MultipartReader instead of ParseMultipartFrom. var multipartByReader = &multipart.Form{ Value: make(map[string][]string), File: make(map[string][]*multipart.FileHeader), } // MultipartReader returns a MIME multipart reader if this is a // multipart/form-data POST request, else returns nil and an error. // Use this function instead of ParseMultipartForm to // process the request body as a stream. func (r *Request) MultipartReader() (*multipart.Reader, error) { if r.MultipartForm == multipartByReader { return nil, errors.New("http: MultipartReader called twice") } if r.MultipartForm != nil { return nil, errors.New("http: multipart handled by ParseMultipartForm") } r.MultipartForm = multipartByReader return r.multipartReader() } func (r *Request) multipartReader() (*multipart.Reader, error) { v := r.Header.Get("Content-Type") if v == "" { return nil, ErrNotMultipart } d, params, err := mime.ParseMediaType(v) if err != nil || d != "multipart/form-data" { return nil, ErrNotMultipart } boundary, ok := params["boundary"] if !ok { return nil, ErrMissingBoundary } return multipart.NewReader(r.Body, boundary), nil } // Return value if nonempty, def otherwise. func valueOrDefault(value, def string) string { if value != "" { return value } return def } // NOTE: This is not intended to reflect the actual Go version being used. // It was changed from "Go http package" to "Go 1.1 package http" at the // time of the Go 1.1 release because the former User-Agent had ended up // on a blacklist for some intrusion detection systems. // See https://codereview.appspot.com/7532043. const defaultUserAgent = "Go 1.1 package http" // Write writes an HTTP/1.1 request -- header and body -- in wire format. // This method consults the following fields of the request: // Host // URL // Method (defaults to "GET") // Header // ContentLength // TransferEncoding // Body // // If Body is present, Content-Length is <= 0 and TransferEncoding // hasn't been set to "identity", Write adds "Transfer-Encoding: // chunked" to the header. Body is closed after it is sent. func (r *Request) Write(w io.Writer) error { return r.write(w, false, nil) } // WriteProxy is like Write but writes the request in the form // expected by an HTTP proxy. In particular, WriteProxy writes the // initial Request-URI line of the request with an absolute URI, per // section 5.1.2 of RFC 2616, including the scheme and host. // In either case, WriteProxy also writes a Host header, using // either r.Host or r.URL.Host. func (r *Request) WriteProxy(w io.Writer) error { return r.write(w, true, nil) } // extraHeaders may be nil func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders http.Header) error { host := req.Host if host == "" { if req.URL == nil { return errors.New("http: Request.Write on Request with no Host or URL set") } host = req.URL.Host } ruri := req.URL.RequestURI() if usingProxy && req.URL.Scheme != "" && req.URL.Opaque == "" { ruri = req.URL.Scheme + "://" + host + ruri } else if req.Method == "CONNECT" && req.URL.Path == "" { // CONNECT requests normally give just the host and port, not a full URL. ruri = host } // TODO(bradfitz): escape at least newlines in ruri? // Wrap the writer in a bufio Writer if it's not already buffered. // Don't always call NewWriter, as that forces a bytes.Buffer // and other small bufio Writers to have a minimum 4k buffer // size. var bw *bufio.Writer if _, ok := w.(io.ByteWriter); !ok { bw = bufio.NewWriter(w) w = bw } fmt.Fprintf(w, "%s %s HTTP/1.1\r\n", valueOrDefault(req.Method, "GET"), ruri) // Header lines fmt.Fprintf(w, "Host: %s\r\n", host) // Use the defaultUserAgent unless the Header contains one, which // may be blank to not send the header. userAgent := defaultUserAgent if req.Header != nil { if ua := req.Header["User-Agent"]; len(ua) > 0 { userAgent = ua[0] } } if userAgent != "" { fmt.Fprintf(w, "User-Agent: %s\r\n", userAgent) } // Process Body,ContentLength,Close,Trailer tw, err := newTransferWriter(req) if err != nil { return err } err = tw.WriteHeader(w) if err != nil { return err } err = req.Header.WriteSubset(w, reqWriteExcludeHeader) if err != nil { return err } if extraHeaders != nil { err = extraHeaders.Write(w) if err != nil { return err } } io.WriteString(w, "\r\n") // Write body and trailer err = tw.WriteBody(w) if err != nil { return err } if bw != nil { return bw.Flush() } return nil } // ParseHTTPVersion parses a HTTP version string. // "HTTP/1.0" returns (1, 0, true). func ParseHTTPVersion(vers string) (major, minor int, ok bool) { const Big = 1000000 // arbitrary upper bound switch vers { case "HTTP/1.1": return 1, 1, true case "HTTP/1.0": return 1, 0, true } if !strings.HasPrefix(vers, "HTTP/") { return 0, 0, false } dot := strings.Index(vers, ".") if dot < 0 { return 0, 0, false } major, err := strconv.Atoi(vers[5:dot]) if err != nil || major < 0 || major > Big { return 0, 0, false } minor, err = strconv.Atoi(vers[dot+1:]) if err != nil || minor < 0 || minor > Big { return 0, 0, false } return major, minor, true } // NewRequest returns a new Request given a method, URL, and optional body. // // If the provided body is also an io.Closer, the returned // Request.Body is set to body and will be closed by the Client // methods Do, Post, and PostForm, and Transport.RoundTrip. func NewRequest(method, urlStr string, body io.Reader) (*Request, error) { u, err := url.Parse(urlStr) if err != nil { return nil, err } rc, ok := body.(io.ReadCloser) if !ok && body != nil { rc = ioutil.NopCloser(body) } req := &Request{ Method: method, URL: u, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: make(http.Header), Body: rc, Host: u.Host, } if body != nil { switch v := body.(type) { case *bytes.Buffer: req.ContentLength = int64(v.Len()) case *bytes.Reader: req.ContentLength = int64(v.Len()) case *strings.Reader: req.ContentLength = int64(v.Len()) } } return req, nil } // SetBasicAuth sets the request's Authorization header to use HTTP // Basic Authentication with the provided username and password. // // With HTTP Basic Authentication the provided username and password // are not encrypted. func (r *Request) SetBasicAuth(username, password string) { r.Header.Set("Authorization", "Basic "+basicAuth(username, password)) } // parseRequestLine parses "GET /foo HTTP/1.1" into its three parts. func parseRequestLine(line string) (method, requestURI, proto string, ok bool) { s1 := strings.Index(line, " ") s2 := strings.Index(line[s1+1:], " ") if s1 < 0 || s2 < 0 { return } s2 += s1 + 1 return line[:s1], line[s1+1 : s2], line[s2+1:], true } // TODO(bradfitz): use a sync.Cache when available var textprotoReaderCache = make(chan *textproto.Reader, 4) func newTextprotoReader(br *bufio.Reader) *textproto.Reader { select { case r := <-textprotoReaderCache: r.R = br return r default: return textproto.NewReader(br) } } func putTextprotoReader(r *textproto.Reader) { r.R = nil select { case textprotoReaderCache <- r: default: } } // ReadRequest reads and parses a request from b. func ReadRequest(b *bufio.Reader) (req *Request, err error) { tp := newTextprotoReader(b) req = new(Request) // First line: GET /index.html HTTP/1.0 var s string if s, err = tp.ReadLine(); err != nil { return nil, err } defer func() { putTextprotoReader(tp) if err == io.EOF { err = io.ErrUnexpectedEOF } }() var ok bool req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s) if !ok { return nil, &badStringError{"malformed HTTP request", s} } rawurl := req.RequestURI if req.ProtoMajor, req.ProtoMinor, ok = ParseHTTPVersion(req.Proto); !ok { return nil, &badStringError{"malformed HTTP version", req.Proto} } // CONNECT requests are used two different ways, and neither uses a full URL: // The standard use is to tunnel HTTPS through an HTTP proxy. // It looks like "CONNECT www.google.com:443 HTTP/1.1", and the parameter is // just the authority section of a URL. This information should go in req.URL.Host. // // The net/rpc package also uses CONNECT, but there the parameter is a path // that starts with a slash. It can be parsed with the regular URL parser, // and the path will end up in req.URL.Path, where it needs to be in order for // RPC to work. justAuthority := req.Method == "CONNECT" && !strings.HasPrefix(rawurl, "/") if justAuthority { rawurl = "http://" + rawurl } if req.URL, err = url.ParseRequestURI(rawurl); err != nil { return nil, err } if justAuthority { // Strip the bogus "http://" back off. req.URL.Scheme = "" } // Subsequent lines: Key: value. mimeHeader, err := tp.ReadMIMEHeader() if err != nil { return nil, err } req.Header = http.Header(mimeHeader) // RFC2616: Must treat // GET /index.html HTTP/1.1 // Host: www.google.com // and // GET http://www.google.com/index.html HTTP/1.1 // Host: doesntmatter // the same. In the second case, any Host line is ignored. req.Host = req.URL.Host if req.Host == "" { req.Host = req.Header.Get("Host") } delete(req.Header, "Host") fixPragmaCacheControl(req.Header) err = readTransfer(req, b) if err != nil { return nil, err } return req, nil } // MaxBytesReader is similar to io.LimitReader but is intended for // limiting the size of incoming request bodies. In contrast to // io.LimitReader, MaxBytesReader's result is a ReadCloser, returns a // non-EOF error for a Read beyond the limit, and Closes the // underlying reader when its Close method is called. // // MaxBytesReader prevents clients from accidentally or maliciously // sending a large request and wasting server resources. func MaxBytesReader(w http.ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser { return &maxBytesReader{w: w, r: r, n: n} } type maxBytesReader struct { w http.ResponseWriter r io.ReadCloser // underlying reader n int64 // max bytes remaining stopped bool } func (l *maxBytesReader) Read(p []byte) (n int, err error) { if l.n <= 0 { if !l.stopped { l.stopped = true } return 0, errors.New("http: request body too large") } if int64(len(p)) > l.n { p = p[:l.n] } n, err = l.r.Read(p) l.n -= int64(n) return } func (l *maxBytesReader) Close() error { return l.r.Close() } func copyValues(dst, src url.Values) { for k, vs := range src { for _, value := range vs { dst.Add(k, value) } } } func parsePostForm(r *Request) (vs url.Values, err error) { if r.Body == nil { err = errors.New("missing form body") return } ct := r.Header.Get("Content-Type") // RFC 2616, section 7.2.1 - empty type // SHOULD be treated as application/octet-stream if ct == "" { ct = "application/octet-stream" } ct, _, err = mime.ParseMediaType(ct) switch { case ct == "application/x-www-form-urlencoded": var reader io.Reader = r.Body maxFormSize := int64(1<<63 - 1) if _, ok := r.Body.(*maxBytesReader); !ok { maxFormSize = int64(10 << 20) // 10 MB is a lot of text. reader = io.LimitReader(r.Body, maxFormSize+1) } b, e := ioutil.ReadAll(reader) if e != nil { if err == nil { err = e } break } if int64(len(b)) > maxFormSize { err = errors.New("http: POST too large") return } vs, e = url.ParseQuery(string(b)) if err == nil { err = e } case ct == "multipart/form-data": // handled by ParseMultipartForm (which is calling us, or should be) // TODO(bradfitz): there are too many possible // orders to call too many functions here. // Clean this up and write more tests. // request_test.go contains the start of this, // in TestParseMultipartFormOrder and others. } return } // ParseForm parses the raw query from the URL and updates r.Form. // // For POST or PUT requests, it also parses the request body as a form and // put the results into both r.PostForm and r.Form. // POST and PUT body parameters take precedence over URL query string values // in r.Form. // // If the request Body's size has not already been limited by MaxBytesReader, // the size is capped at 10MB. // // ParseMultipartForm calls ParseForm automatically. // It is idempotent. func (r *Request) ParseForm() error { var err error if r.PostForm == nil { if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" { r.PostForm, err = parsePostForm(r) } if r.PostForm == nil { r.PostForm = make(url.Values) } } if r.Form == nil { if len(r.PostForm) > 0 { r.Form = make(url.Values) copyValues(r.Form, r.PostForm) } var newValues url.Values if r.URL != nil { var e error newValues, e = url.ParseQuery(r.URL.RawQuery) if err == nil { err = e } } if newValues == nil { newValues = make(url.Values) } if r.Form == nil { r.Form = newValues } else { copyValues(r.Form, newValues) } } return err } // ParseMultipartForm parses a request body as multipart/form-data. // The whole request body is parsed and up to a total of maxMemory bytes of // its file parts are stored in memory, with the remainder stored on // disk in temporary files. // ParseMultipartForm calls ParseForm if necessary. // After one call to ParseMultipartForm, subsequent calls have no effect. func (r *Request) ParseMultipartForm(maxMemory int64) error { if r.MultipartForm == multipartByReader { return errors.New("http: multipart handled by MultipartReader") } if r.Form == nil { err := r.ParseForm() if err != nil { return err } } if r.MultipartForm != nil { return nil } mr, err := r.multipartReader() if err != nil { return err } f, err := mr.ReadForm(maxMemory) if err != nil { return err } for k, v := range f.Value { r.Form[k] = append(r.Form[k], v...) } r.MultipartForm = f return nil } // FormValue returns the first value for the named component of the query. // POST and PUT body parameters take precedence over URL query string values. // FormValue calls ParseMultipartForm and ParseForm if necessary. // To access multiple values of the same key use ParseForm. func (r *Request) FormValue(key string) string { if r.Form == nil { r.ParseMultipartForm(defaultMaxMemory) } if vs := r.Form[key]; len(vs) > 0 { return vs[0] } return "" } // PostFormValue returns the first value for the named component of the POST // or PUT request body. URL query parameters are ignored. // PostFormValue calls ParseMultipartForm and ParseForm if necessary. func (r *Request) PostFormValue(key string) string { if r.PostForm == nil { r.ParseMultipartForm(defaultMaxMemory) } if vs := r.PostForm[key]; len(vs) > 0 { return vs[0] } return "" } // FormFile returns the first file for the provided form key. // FormFile calls ParseMultipartForm and ParseForm if necessary. func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) { if r.MultipartForm == multipartByReader { return nil, nil, errors.New("http: multipart handled by MultipartReader") } if r.MultipartForm == nil { err := r.ParseMultipartForm(defaultMaxMemory) if err != nil { return nil, nil, err } } if r.MultipartForm != nil && r.MultipartForm.File != nil { if fhs := r.MultipartForm.File[key]; len(fhs) > 0 { f, err := fhs[0].Open() return f, fhs[0], err } } return nil, nil, ErrMissingFile } func (r *Request) expectsContinue() bool { return hasToken(r.Header.Get("Expect"), "100-continue") } func (r *Request) wantsHttp10KeepAlive() bool { if r.ProtoMajor != 1 || r.ProtoMinor != 0 { return false } return hasToken(r.Header.Get("Connection"), "keep-alive") } func (r *Request) wantsClose() bool { return hasToken(r.Header.Get("Connection"), "close") } func (r *Request) closeBody() { if r.Body != nil { r.Body.Close() } } ubuntu-push-0.68+16.04.20160310.2/http13client/transfer.go0000644000015600001650000004445112670364255023162 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "bufio" "bytes" "errors" "fmt" "io" "io/ioutil" "net/http" "net/textproto" "sort" "strconv" "strings" "sync" ) type errorReader struct { err error } func (r *errorReader) Read(p []byte) (n int, err error) { return 0, r.err } // transferWriter inspects the fields of a user-supplied Request or Response, // sanitizes them without changing the user object and provides methods for // writing the respective header, body and trailer in wire format. type transferWriter struct { Method string Body io.Reader BodyCloser io.Closer ResponseToHEAD bool ContentLength int64 // -1 means unknown, 0 means exactly none Close bool TransferEncoding []string Trailer http.Header } func newTransferWriter(r interface{}) (t *transferWriter, err error) { t = &transferWriter{} // Extract relevant fields atLeastHTTP11 := false switch rr := r.(type) { case *Request: if rr.ContentLength != 0 && rr.Body == nil { return nil, fmt.Errorf("http: Request.ContentLength=%d with nil Body", rr.ContentLength) } t.Method = rr.Method t.Body = rr.Body t.BodyCloser = rr.Body t.ContentLength = rr.ContentLength t.Close = rr.Close t.TransferEncoding = rr.TransferEncoding t.Trailer = rr.Trailer atLeastHTTP11 = rr.ProtoAtLeast(1, 1) if t.Body != nil && len(t.TransferEncoding) == 0 && atLeastHTTP11 { if t.ContentLength == 0 { // Test to see if it's actually zero or just unset. var buf [1]byte n, rerr := io.ReadFull(t.Body, buf[:]) if rerr != nil && rerr != io.EOF { t.ContentLength = -1 t.Body = &errorReader{rerr} } else if n == 1 { // Oh, guess there is data in this Body Reader after all. // The ContentLength field just wasn't set. // Stich the Body back together again, re-attaching our // consumed byte. t.ContentLength = -1 t.Body = io.MultiReader(bytes.NewReader(buf[:]), t.Body) } else { // Body is actually empty. t.Body = nil t.BodyCloser = nil } } if t.ContentLength < 0 { t.TransferEncoding = []string{"chunked"} } } case *Response: if rr.Request != nil { t.Method = rr.Request.Method } t.Body = rr.Body t.BodyCloser = rr.Body t.ContentLength = rr.ContentLength t.Close = rr.Close t.TransferEncoding = rr.TransferEncoding t.Trailer = rr.Trailer atLeastHTTP11 = rr.ProtoAtLeast(1, 1) t.ResponseToHEAD = noBodyExpected(t.Method) } // Sanitize Body,ContentLength,TransferEncoding if t.ResponseToHEAD { t.Body = nil if chunked(t.TransferEncoding) { t.ContentLength = -1 } } else { if !atLeastHTTP11 || t.Body == nil { t.TransferEncoding = nil } if chunked(t.TransferEncoding) { t.ContentLength = -1 } else if t.Body == nil { // no chunking, no body t.ContentLength = 0 } } // Sanitize Trailer if !chunked(t.TransferEncoding) { t.Trailer = nil } return t, nil } func noBodyExpected(requestMethod string) bool { return requestMethod == "HEAD" } func (t *transferWriter) shouldSendContentLength() bool { if chunked(t.TransferEncoding) { return false } if t.ContentLength > 0 { return true } // Many servers expect a Content-Length for these methods if t.Method == "POST" || t.Method == "PUT" { return true } if t.ContentLength == 0 && isIdentity(t.TransferEncoding) { return true } return false } func (t *transferWriter) WriteHeader(w io.Writer) error { if t.Close { if _, err := io.WriteString(w, "Connection: close\r\n"); err != nil { return err } } // Write Content-Length and/or Transfer-Encoding whose values are a // function of the sanitized field triple (Body, ContentLength, // TransferEncoding) if t.shouldSendContentLength() { if _, err := io.WriteString(w, "Content-Length: "); err != nil { return err } if _, err := io.WriteString(w, strconv.FormatInt(t.ContentLength, 10)+"\r\n"); err != nil { return err } } else if chunked(t.TransferEncoding) { if _, err := io.WriteString(w, "Transfer-Encoding: chunked\r\n"); err != nil { return err } } // Write Trailer header if t.Trailer != nil { keys := make([]string, 0, len(t.Trailer)) for k := range t.Trailer { k = http.CanonicalHeaderKey(k) switch k { case "Transfer-Encoding", "Trailer", "Content-Length": return &badStringError{"invalid Trailer key", k} } keys = append(keys, k) } if len(keys) > 0 { sort.Strings(keys) // TODO: could do better allocation-wise here, but trailers are rare, // so being lazy for now. if _, err := io.WriteString(w, "Trailer: "+strings.Join(keys, ",")+"\r\n"); err != nil { return err } } } return nil } func (t *transferWriter) WriteBody(w io.Writer) error { var err error var ncopy int64 // Write body if t.Body != nil { if chunked(t.TransferEncoding) { cw := newChunkedWriter(w) _, err = io.Copy(cw, t.Body) if err == nil { err = cw.Close() } } else if t.ContentLength == -1 { ncopy, err = io.Copy(w, t.Body) } else { ncopy, err = io.Copy(w, io.LimitReader(t.Body, t.ContentLength)) if err != nil { return err } var nextra int64 nextra, err = io.Copy(ioutil.Discard, t.Body) ncopy += nextra } if err != nil { return err } if err = t.BodyCloser.Close(); err != nil { return err } } if !t.ResponseToHEAD && t.ContentLength != -1 && t.ContentLength != ncopy { return fmt.Errorf("http: Request.ContentLength=%d with Body length %d", t.ContentLength, ncopy) } // TODO(petar): Place trailer writer code here. if chunked(t.TransferEncoding) { // Write Trailer header if t.Trailer != nil { if err := t.Trailer.Write(w); err != nil { return err } } // Last chunk, empty trailer _, err = io.WriteString(w, "\r\n") } return err } type transferReader struct { // Input Header http.Header StatusCode int RequestMethod string ProtoMajor int ProtoMinor int // Output Body io.ReadCloser ContentLength int64 TransferEncoding []string Close bool Trailer http.Header } // bodyAllowedForStatus reports whether a given response status code // permits a body. See RFC2616, section 4.4. func bodyAllowedForStatus(status int) bool { switch { case status >= 100 && status <= 199: return false case status == 204: return false case status == 304: return false } return true } var ( suppressedHeaders304 = []string{"Content-Type", "Content-Length", "Transfer-Encoding"} suppressedHeadersNoBody = []string{"Content-Length", "Transfer-Encoding"} ) func suppressedHeaders(status int) []string { switch { case status == 304: // RFC 2616 section 10.3.5: "the response MUST NOT include other entity-headers" return suppressedHeaders304 case !bodyAllowedForStatus(status): return suppressedHeadersNoBody } return nil } // msg is *Request or *Response. func readTransfer(msg interface{}, r *bufio.Reader) (err error) { t := &transferReader{RequestMethod: "GET"} // Unify input isResponse := false switch rr := msg.(type) { case *Response: t.Header = rr.Header t.StatusCode = rr.StatusCode t.ProtoMajor = rr.ProtoMajor t.ProtoMinor = rr.ProtoMinor t.Close = shouldClose(t.ProtoMajor, t.ProtoMinor, t.Header) isResponse = true if rr.Request != nil { t.RequestMethod = rr.Request.Method } case *Request: t.Header = rr.Header t.ProtoMajor = rr.ProtoMajor t.ProtoMinor = rr.ProtoMinor // Transfer semantics for Requests are exactly like those for // Responses with status code 200, responding to a GET method t.StatusCode = 200 default: panic("unexpected type") } // Default to HTTP/1.1 if t.ProtoMajor == 0 && t.ProtoMinor == 0 { t.ProtoMajor, t.ProtoMinor = 1, 1 } // Transfer encoding, content length t.TransferEncoding, err = fixTransferEncoding(t.RequestMethod, t.Header) if err != nil { return err } realLength, err := fixLength(isResponse, t.StatusCode, t.RequestMethod, t.Header, t.TransferEncoding) if err != nil { return err } if isResponse && t.RequestMethod == "HEAD" { if n, err := parseContentLength(t.Header.Get("Content-Length")); err != nil { return err } else { t.ContentLength = n } } else { t.ContentLength = realLength } // Trailer t.Trailer, err = fixTrailer(t.Header, t.TransferEncoding) if err != nil { return err } // If there is no Content-Length or chunked Transfer-Encoding on a *Response // and the status is not 1xx, 204 or 304, then the body is unbounded. // See RFC2616, section 4.4. switch msg.(type) { case *Response: if realLength == -1 && !chunked(t.TransferEncoding) && bodyAllowedForStatus(t.StatusCode) { // Unbounded body. t.Close = true } } // Prepare body reader. ContentLength < 0 means chunked encoding // or close connection when finished, since multipart is not supported yet switch { case chunked(t.TransferEncoding): if noBodyExpected(t.RequestMethod) { t.Body = eofReader } else { t.Body = &body{src: newChunkedReader(r), hdr: msg, r: r, closing: t.Close} } case realLength == 0: t.Body = eofReader case realLength > 0: t.Body = &body{src: io.LimitReader(r, realLength), closing: t.Close} default: // realLength < 0, i.e. "Content-Length" not mentioned in header if t.Close { // Close semantics (i.e. HTTP/1.0) t.Body = &body{src: r, closing: t.Close} } else { // Persistent connection (i.e. HTTP/1.1) t.Body = eofReader } } // Unify output switch rr := msg.(type) { case *Request: rr.Body = t.Body rr.ContentLength = t.ContentLength rr.TransferEncoding = t.TransferEncoding rr.Close = t.Close rr.Trailer = t.Trailer case *Response: rr.Body = t.Body rr.ContentLength = t.ContentLength rr.TransferEncoding = t.TransferEncoding rr.Close = t.Close rr.Trailer = t.Trailer } return nil } // Checks whether chunked is part of the encodings stack func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" } // Checks whether the encoding is explicitly "identity". func isIdentity(te []string) bool { return len(te) == 1 && te[0] == "identity" } // Sanitize transfer encoding func fixTransferEncoding(requestMethod string, header http.Header) ([]string, error) { raw, present := header["Transfer-Encoding"] if !present { return nil, nil } delete(header, "Transfer-Encoding") encodings := strings.Split(raw[0], ",") te := make([]string, 0, len(encodings)) // TODO: Even though we only support "identity" and "chunked" // encodings, the loop below is designed with foresight. One // invariant that must be maintained is that, if present, // chunked encoding must always come first. for _, encoding := range encodings { encoding = strings.ToLower(strings.TrimSpace(encoding)) // "identity" encoding is not recorded if encoding == "identity" { break } if encoding != "chunked" { return nil, &badStringError{"unsupported transfer encoding", encoding} } te = te[0 : len(te)+1] te[len(te)-1] = encoding } if len(te) > 1 { return nil, &badStringError{"too many transfer encodings", strings.Join(te, ",")} } if len(te) > 0 { // Chunked encoding trumps Content-Length. See RFC 2616 // Section 4.4. Currently len(te) > 0 implies chunked // encoding. delete(header, "Content-Length") return te, nil } return nil, nil } // Determine the expected body length, using RFC 2616 Section 4.4. This // function is not a method, because ultimately it should be shared by // ReadResponse and ReadRequest. func fixLength(isResponse bool, status int, requestMethod string, header http.Header, te []string) (int64, error) { // Logic based on response type or status if noBodyExpected(requestMethod) { return 0, nil } if status/100 == 1 { return 0, nil } switch status { case 204, 304: return 0, nil } // Logic based on Transfer-Encoding if chunked(te) { return -1, nil } // Logic based on Content-Length cl := strings.TrimSpace(header.Get("Content-Length")) if cl != "" { n, err := parseContentLength(cl) if err != nil { return -1, err } return n, nil } else { header.Del("Content-Length") } if !isResponse && requestMethod == "GET" { // RFC 2616 doesn't explicitly permit nor forbid an // entity-body on a GET request so we permit one if // declared, but we default to 0 here (not -1 below) // if there's no mention of a body. return 0, nil } // Body-EOF logic based on other methods (like closing, or chunked coding) return -1, nil } // Determine whether to hang up after sending a request and body, or // receiving a response and body // 'header' is the request headers func shouldClose(major, minor int, header http.Header) bool { if major < 1 { return true } else if major == 1 && minor == 0 { if !strings.Contains(strings.ToLower(header.Get("Connection")), "keep-alive") { return true } return false } else { // TODO: Should split on commas, toss surrounding white space, // and check each field. if strings.ToLower(header.Get("Connection")) == "close" { header.Del("Connection") return true } } return false } // Parse the trailer header func fixTrailer(header http.Header, te []string) (http.Header, error) { raw := header.Get("Trailer") if raw == "" { return nil, nil } header.Del("Trailer") trailer := make(http.Header) keys := strings.Split(raw, ",") for _, key := range keys { key = http.CanonicalHeaderKey(strings.TrimSpace(key)) switch key { case "Transfer-Encoding", "Trailer", "Content-Length": return nil, &badStringError{"bad trailer key", key} } trailer[key] = nil } if len(trailer) == 0 { return nil, nil } if !chunked(te) { // Trailer and no chunking return nil, ErrUnexpectedTrailer } return trailer, nil } // body turns a Reader into a ReadCloser. // Close ensures that the body has been fully read // and then reads the trailer if necessary. type body struct { src io.Reader hdr interface{} // non-nil (Response or Request) value means read trailer r *bufio.Reader // underlying wire-format reader for the trailer closing bool // is the connection to be closed after reading body? mu sync.Mutex // guards closed, and calls to Read and Close closed bool } // ErrBodyReadAfterClose is returned when reading a Request or Response // Body after the body has been closed. This typically happens when the body is // read after an HTTP Handler calls WriteHeader or Write on its // ResponseWriter. var ErrBodyReadAfterClose = errors.New("http: invalid Read on closed Body") func (b *body) Read(p []byte) (n int, err error) { b.mu.Lock() defer b.mu.Unlock() if b.closed { return 0, ErrBodyReadAfterClose } return b.readLocked(p) } // Must hold b.mu. func (b *body) readLocked(p []byte) (n int, err error) { n, err = b.src.Read(p) if err == io.EOF { // Chunked case. Read the trailer. if b.hdr != nil { if e := b.readTrailer(); e != nil { err = e } b.hdr = nil } else { // If the server declared the Content-Length, our body is a LimitedReader // and we need to check whether this EOF arrived early. if lr, ok := b.src.(*io.LimitedReader); ok && lr.N > 0 { err = io.ErrUnexpectedEOF } } } // If we can return an EOF here along with the read data, do // so. This is optional per the io.Reader contract, but doing // so helps the HTTP transport code recycle its connection // earlier (since it will see this EOF itself), even if the // client doesn't do future reads or Close. if err == nil && n > 0 { if lr, ok := b.src.(*io.LimitedReader); ok && lr.N == 0 { err = io.EOF } } return n, err } var ( singleCRLF = []byte("\r\n") doubleCRLF = []byte("\r\n\r\n") ) func seeUpcomingDoubleCRLF(r *bufio.Reader) bool { for peekSize := 4; ; peekSize++ { // This loop stops when Peek returns an error, // which it does when r's buffer has been filled. buf, err := r.Peek(peekSize) if bytes.HasSuffix(buf, doubleCRLF) { return true } if err != nil { break } } return false } var errTrailerEOF = errors.New("http: unexpected EOF reading trailer") func (b *body) readTrailer() error { // The common case, since nobody uses trailers. buf, err := b.r.Peek(2) if bytes.Equal(buf, singleCRLF) { b.r.ReadByte() b.r.ReadByte() return nil } if len(buf) < 2 { return errTrailerEOF } if err != nil { return err } // Make sure there's a header terminator coming up, to prevent // a DoS with an unbounded size Trailer. It's not easy to // slip in a LimitReader here, as textproto.NewReader requires // a concrete *bufio.Reader. Also, we can't get all the way // back up to our conn's LimitedReader that *might* be backing // this bufio.Reader. Instead, a hack: we iteratively Peek up // to the bufio.Reader's max size, looking for a double CRLF. // This limits the trailer to the underlying buffer size, typically 4kB. if !seeUpcomingDoubleCRLF(b.r) { return errors.New("http: suspiciously long trailer after chunked body") } hdr, err := textproto.NewReader(b.r).ReadMIMEHeader() if err != nil { if err == io.EOF { return errTrailerEOF } return err } switch rr := b.hdr.(type) { case *Request: mergeSetHeader(&rr.Trailer, http.Header(hdr)) case *Response: mergeSetHeader(&rr.Trailer, http.Header(hdr)) } return nil } func mergeSetHeader(dst *http.Header, src http.Header) { if *dst == nil { *dst = src return } for k, vv := range src { (*dst)[k] = vv } } func (b *body) Close() error { b.mu.Lock() defer b.mu.Unlock() if b.closed { return nil } var err error switch { case b.hdr == nil && b.closing: // no trailer and closing the connection next. // no point in reading to EOF. default: // Fully consume the body, which will also lead to us reading // the trailer headers after the body, if present. _, err = io.Copy(ioutil.Discard, bodyLocked{b}) } b.closed = true return err } // bodyLocked is a io.Reader reading from a *body when its mutex is // already held. type bodyLocked struct { b *body } func (bl bodyLocked) Read(p []byte) (n int, err error) { if bl.b.closed { return 0, ErrBodyReadAfterClose } return bl.b.readLocked(p) } // parseContentLength trims whitespace from s and returns -1 if no value // is set, or the value if it's >= 0. func parseContentLength(cl string) (int64, error) { cl = strings.TrimSpace(cl) if cl == "" { return -1, nil } n, err := strconv.ParseInt(cl, 10, 64) if err != nil || n < 0 { return 0, &badStringError{"bad Content-Length", cl} } return n, nil } ubuntu-push-0.68+16.04.20160310.2/http13client/z_last_test.go0000644000015600001650000000525712670364255023672 0ustar pbuserpbgroup00000000000000// Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http_test import ( "net/http" "runtime" "sort" "strings" "testing" "time" ) func interestingGoroutines() (gs []string) { buf := make([]byte, 2<<20) buf = buf[:runtime.Stack(buf, true)] for _, g := range strings.Split(string(buf), "\n\n") { sl := strings.SplitN(g, "\n", 2) if len(sl) != 2 { continue } stack := strings.TrimSpace(sl[1]) if stack == "" || strings.Contains(stack, "created by net.startServer") || strings.Contains(stack, "created by testing.RunTests") || strings.Contains(stack, "closeWriteAndWait") || strings.Contains(stack, "main.main(") || strings.Contains(stack, "testing.Main(") || // These only show up with GOTRACEBACK=2; Issue 5005 (comment 28) strings.Contains(stack, "runtime.goexit") || strings.Contains(stack, "created by runtime.gc") || strings.Contains(stack, "runtime.MHeap_Scavenger") { continue } gs = append(gs, stack) } sort.Strings(gs) return } // Verify the other tests didn't leave any goroutines running. // This is in a file named z_last_test.go so it sorts at the end. func TestGoroutinesRunning(t *testing.T) { if testing.Short() { t.Skip("not counting goroutines for leakage in -short mode") } gs := interestingGoroutines() n := 0 stackCount := make(map[string]int) for _, g := range gs { stackCount[g]++ n++ } t.Logf("num goroutines = %d", n) if n > 0 { t.Error("Too many goroutines.") for stack, count := range stackCount { t.Logf("%d instances of:\n%s", count, stack) } } } func afterTest(t *testing.T) { http.DefaultTransport.(*http.Transport).CloseIdleConnections() if testing.Short() { return } var bad string badSubstring := map[string]string{ ").readLoop(": "a Transport", ").writeLoop(": "a Transport", "created by net/http/httptest.(*Server).Start": "an httptest.Server", "timeoutHandler": "a TimeoutHandler", "net.(*netFD).connect(": "a timing out dial", ").noteClientGone(": "a closenotifier sender", } var stacks string for i := 0; i < 4; i++ { bad = "" stacks = strings.Join(interestingGoroutines(), "\n\n") for substr, what := range badSubstring { if strings.Contains(stacks, substr) { bad = what } } if bad == "" { return } // Bad stuff found, but goroutines might just still be // shutting down, so give it some time. time.Sleep(250 * time.Millisecond) } t.Errorf("Test appears to have leaked %s:\n%s", bad, stacks) } ubuntu-push-0.68+16.04.20160310.2/http13client/client.go0000644000015600001650000003465512670364255022621 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // HTTP client. See RFC 2616. // // This is the high-level Client interface. // The low-level implementation is in transport.go. package http import ( "encoding/base64" "errors" "fmt" "io" "io/ioutil" "log" "net/http" "net/url" "strings" "sync" "time" ) // A Client is an HTTP client. Its zero value (DefaultClient) is a // usable client that uses DefaultTransport. // // The Client's Transport typically has internal state (cached TCP // connections), so Clients should be reused instead of created as // needed. Clients are safe for concurrent use by multiple goroutines. // // A Client is higher-level than a RoundTripper (such as Transport) // and additionally handles HTTP details such as cookies and // redirects. type Client struct { // Transport specifies the mechanism by which individual // HTTP requests are made. // If nil, DefaultTransport is used. Transport RoundTripper // CheckRedirect specifies the policy for handling redirects. // If CheckRedirect is not nil, the client calls it before // following an HTTP redirect. The arguments req and via are // the upcoming request and the requests made already, oldest // first. If CheckRedirect returns an error, the Client's Get // method returns both the previous Response and // CheckRedirect's error (wrapped in a url.Error) instead of // issuing the Request req. // // If CheckRedirect is nil, the Client uses its default policy, // which is to stop after 10 consecutive requests. CheckRedirect func(req *Request, via []*Request) error // Jar specifies the cookie jar. // If Jar is nil, cookies are not sent in requests and ignored // in responses. Jar http.CookieJar // Timeout specifies a time limit for requests made by this // Client. The timeout includes connection time, any // redirects, and reading the response body. The timer remains // running after Get, Head, Post, or Do return and will // interrupt reading of the Response.Body. // // A Timeout of zero means no timeout. // // The Client's Transport must support the CancelRequest // method or Client will return errors when attempting to make // a request with Get, Head, Post, or Do. Client's default // Transport (DefaultTransport) supports CancelRequest. Timeout time.Duration } // DefaultClient is the default Client and is used by Get, Head, and Post. var DefaultClient = &Client{} // RoundTripper is an interface representing the ability to execute a // single HTTP transaction, obtaining the Response for a given Request. // // A RoundTripper must be safe for concurrent use by multiple // goroutines. type RoundTripper interface { // RoundTrip executes a single HTTP transaction, returning // the Response for the request req. RoundTrip should not // attempt to interpret the response. In particular, // RoundTrip must return err == nil if it obtained a response, // regardless of the response's HTTP status code. A non-nil // err should be reserved for failure to obtain a response. // Similarly, RoundTrip should not attempt to handle // higher-level protocol details such as redirects, // authentication, or cookies. // // RoundTrip should not modify the request, except for // consuming and closing the Body, including on errors. The // request's URL and Header fields are guaranteed to be // initialized. RoundTrip(*Request) (*Response, error) } // Given a string of the form "host", "host:port", or "[ipv6::address]:port", // return true if the string includes a port. func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } // Used in Send to implement io.ReadCloser by bundling together the // bufio.Reader through which we read the response, and the underlying // network connection. type readClose struct { io.Reader io.Closer } func (c *Client) send(req *Request) (*Response, error) { if c.Jar != nil { for _, cookie := range c.Jar.Cookies(req.URL) { req.AddCookie(cookie) } } resp, err := send(req, c.transport()) if err != nil { return nil, err } if c.Jar != nil { if rc := resp.Cookies(); len(rc) > 0 { c.Jar.SetCookies(req.URL, rc) } } return resp, err } // Do sends an HTTP request and returns an HTTP response, following // policy (e.g. redirects, cookies, auth) as configured on the client. // // An error is returned if caused by client policy (such as // CheckRedirect), or if there was an HTTP protocol error. // A non-2xx response doesn't cause an error. // // When err is nil, resp always contains a non-nil resp.Body. // // Callers should close resp.Body when done reading from it. If // resp.Body is not closed, the Client's underlying RoundTripper // (typically Transport) may not be able to re-use a persistent TCP // connection to the server for a subsequent "keep-alive" request. // // The request Body, if non-nil, will be closed by the underlying // Transport, even on errors. // // Generally Get, Post, or PostForm will be used instead of Do. func (c *Client) Do(req *Request) (resp *Response, err error) { if req.Method == "GET" || req.Method == "HEAD" { return c.doFollowingRedirects(req, shouldRedirectGet) } if req.Method == "POST" || req.Method == "PUT" { return c.doFollowingRedirects(req, shouldRedirectPost) } return c.send(req) } func (c *Client) transport() RoundTripper { if c.Transport != nil { return c.Transport } return DefaultTransport } // send issues an HTTP request. // Caller should close resp.Body when done reading from it. func send(req *Request, t RoundTripper) (resp *Response, err error) { if t == nil { req.closeBody() return nil, errors.New("http: no Client.Transport or DefaultTransport") } if req.URL == nil { req.closeBody() return nil, errors.New("http: nil Request.URL") } if req.RequestURI != "" { req.closeBody() return nil, errors.New("http: Request.RequestURI can't be set in client requests.") } // Most the callers of send (Get, Post, et al) don't need // Headers, leaving it uninitialized. We guarantee to the // Transport that this has been initialized, though. if req.Header == nil { req.Header = make(http.Header) } if u := req.URL.User; u != nil { username := u.Username() password, _ := u.Password() req.Header.Set("Authorization", "Basic "+basicAuth(username, password)) } resp, err = t.RoundTrip(req) if err != nil { if resp != nil { log.Printf("RoundTripper returned a response & error; ignoring response") } return nil, err } return resp, nil } // See 2 (end of page 4) http://www.ietf.org/rfc/rfc2617.txt // "To receive authorization, the client sends the userid and password, // separated by a single colon (":") character, within a base64 // encoded string in the credentials." // It is not meant to be urlencoded. func basicAuth(username, password string) string { auth := username + ":" + password return base64.StdEncoding.EncodeToString([]byte(auth)) } // True if the specified HTTP status code is one for which the Get utility should // automatically redirect. func shouldRedirectGet(statusCode int) bool { switch statusCode { case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect: return true } return false } // True if the specified HTTP status code is one for which the Post utility should // automatically redirect. func shouldRedirectPost(statusCode int) bool { switch statusCode { case http.StatusFound, http.StatusSeeOther: return true } return false } // Get issues a GET to the specified URL. If the response is one of the following // redirect codes, Get follows the redirect, up to a maximum of 10 redirects: // // 301 (Moved Permanently) // 302 (Found) // 303 (See Other) // 307 (Temporary Redirect) // // An error is returned if there were too many redirects or if there // was an HTTP protocol error. A non-2xx response doesn't cause an // error. // // When err is nil, resp always contains a non-nil resp.Body. // Caller should close resp.Body when done reading from it. // // Get is a wrapper around DefaultClient.Get. func Get(url string) (resp *Response, err error) { return DefaultClient.Get(url) } // Get issues a GET to the specified URL. If the response is one of the // following redirect codes, Get follows the redirect after calling the // Client's CheckRedirect function. // // 301 (Moved Permanently) // 302 (Found) // 303 (See Other) // 307 (Temporary Redirect) // // An error is returned if the Client's CheckRedirect function fails // or if there was an HTTP protocol error. A non-2xx response doesn't // cause an error. // // When err is nil, resp always contains a non-nil resp.Body. // Caller should close resp.Body when done reading from it. func (c *Client) Get(url string) (resp *Response, err error) { req, err := NewRequest("GET", url, nil) if err != nil { return nil, err } return c.doFollowingRedirects(req, shouldRedirectGet) } func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bool) (resp *Response, err error) { var base *url.URL redirectChecker := c.CheckRedirect if redirectChecker == nil { redirectChecker = defaultCheckRedirect } var via []*Request if ireq.URL == nil { ireq.closeBody() return nil, errors.New("http: nil Request.URL") } var reqmu sync.Mutex // guards req req := ireq var timer *time.Timer if c.Timeout > 0 { type canceler interface { CancelRequest(*Request) } tr, ok := c.transport().(canceler) if !ok { return nil, fmt.Errorf("net/http: Client Transport of type %T doesn't support CancelRequest; Timeout not supported", c.transport()) } timer = time.AfterFunc(c.Timeout, func() { reqmu.Lock() defer reqmu.Unlock() tr.CancelRequest(req) }) } urlStr := "" // next relative or absolute URL to fetch (after first request) redirectFailed := false for redirect := 0; ; redirect++ { if redirect != 0 { nreq := new(Request) nreq.Method = ireq.Method if ireq.Method == "POST" || ireq.Method == "PUT" { nreq.Method = "GET" } nreq.Header = make(http.Header) nreq.URL, err = base.Parse(urlStr) if err != nil { break } if len(via) > 0 { // Add the Referer header. lastReq := via[len(via)-1] if lastReq.URL.Scheme != "https" { nreq.Header.Set("Referer", lastReq.URL.String()) } err = redirectChecker(nreq, via) if err != nil { redirectFailed = true break } } reqmu.Lock() req = nreq reqmu.Unlock() } urlStr = req.URL.String() if resp, err = c.send(req); err != nil { break } if shouldRedirect(resp.StatusCode) { // Read the body if small so underlying TCP connection will be re-used. // No need to check for errors: if it fails, Transport won't reuse it anyway. const maxBodySlurpSize = 2 << 10 if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize { io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize) } resp.Body.Close() if urlStr = resp.Header.Get("Location"); urlStr == "" { err = errors.New(fmt.Sprintf("%d response missing Location header", resp.StatusCode)) break } base = req.URL via = append(via, req) continue } if timer != nil { resp.Body = &cancelTimerBody{timer, resp.Body} } return resp, nil } method := ireq.Method urlErr := &url.Error{ Op: method[0:1] + strings.ToLower(method[1:]), URL: urlStr, Err: err, } if redirectFailed { // Special case for Go 1 compatibility: return both the response // and an error if the CheckRedirect function failed. // See http://golang.org/issue/3795 return resp, urlErr } if resp != nil { resp.Body.Close() } return nil, urlErr } func defaultCheckRedirect(req *Request, via []*Request) error { if len(via) >= 10 { return errors.New("stopped after 10 redirects") } return nil } // Post issues a POST to the specified URL. // // Caller should close resp.Body when done reading from it. // // Post is a wrapper around DefaultClient.Post func Post(url string, bodyType string, body io.Reader) (resp *Response, err error) { return DefaultClient.Post(url, bodyType, body) } // Post issues a POST to the specified URL. // // Caller should close resp.Body when done reading from it. // // If the provided body is also an io.Closer, it is closed after the // request. func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *Response, err error) { req, err := NewRequest("POST", url, body) if err != nil { return nil, err } req.Header.Set("Content-Type", bodyType) return c.doFollowingRedirects(req, shouldRedirectPost) } // PostForm issues a POST to the specified URL, with data's keys and // values URL-encoded as the request body. // // When err is nil, resp always contains a non-nil resp.Body. // Caller should close resp.Body when done reading from it. // // PostForm is a wrapper around DefaultClient.PostForm func PostForm(url string, data url.Values) (resp *Response, err error) { return DefaultClient.PostForm(url, data) } // PostForm issues a POST to the specified URL, // with data's keys and values urlencoded as the request body. // // When err is nil, resp always contains a non-nil resp.Body. // Caller should close resp.Body when done reading from it. func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error) { return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) } // Head issues a HEAD to the specified URL. If the response is one of the // following redirect codes, Head follows the redirect after calling the // Client's CheckRedirect function. // // 301 (Moved Permanently) // 302 (Found) // 303 (See Other) // 307 (Temporary Redirect) // // Head is a wrapper around DefaultClient.Head func Head(url string) (resp *Response, err error) { return DefaultClient.Head(url) } // Head issues a HEAD to the specified URL. If the response is one of the // following redirect codes, Head follows the redirect after calling the // Client's CheckRedirect function. // // 301 (Moved Permanently) // 302 (Found) // 303 (See Other) // 307 (Temporary Redirect) func (c *Client) Head(url string) (resp *Response, err error) { req, err := NewRequest("HEAD", url, nil) if err != nil { return nil, err } return c.doFollowingRedirects(req, shouldRedirectGet) } type cancelTimerBody struct { t *time.Timer rc io.ReadCloser } func (b *cancelTimerBody) Read(p []byte) (n int, err error) { n, err = b.rc.Read(p) if err == io.EOF { b.t.Stop() } return } func (b *cancelTimerBody) Close() error { err := b.rc.Close() b.t.Stop() return err } ubuntu-push-0.68+16.04.20160310.2/http13client/_using.txt0000644000015600001650000000015712670364255023027 0ustar pbuserpbgroup00000000000000parent: 20169:9895f9e36435 go1.3 release go1.3 branch: release-branch.go1.3 commit: (clean) update: (current) ubuntu-push-0.68+16.04.20160310.2/http13client/chunked.go0000644000015600001650000001174612670364255022760 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // The wire protocol for HTTP's "chunked" Transfer-Encoding. // This code is duplicated in net/http and net/http/httputil. // Please make any changes in both files. package http import ( "bufio" "bytes" "errors" "fmt" "io" ) const maxLineLength = 4096 // assumed <= bufio.defaultBufSize var ErrLineTooLong = errors.New("header line too long") // newChunkedReader returns a new chunkedReader that translates the data read from r // out of HTTP "chunked" format before returning it. // The chunkedReader returns io.EOF when the final 0-length chunk is read. // // newChunkedReader is not needed by normal applications. The http package // automatically decodes chunking when reading response bodies. func newChunkedReader(r io.Reader) io.Reader { br, ok := r.(*bufio.Reader) if !ok { br = bufio.NewReader(r) } return &chunkedReader{r: br} } type chunkedReader struct { r *bufio.Reader n uint64 // unread bytes in chunk err error buf [2]byte } func (cr *chunkedReader) beginChunk() { // chunk-size CRLF var line []byte line, cr.err = readLine(cr.r) if cr.err != nil { return } cr.n, cr.err = parseHexUint(line) if cr.err != nil { return } if cr.n == 0 { cr.err = io.EOF } } func (cr *chunkedReader) chunkHeaderAvailable() bool { n := cr.r.Buffered() if n > 0 { peek, _ := cr.r.Peek(n) return bytes.IndexByte(peek, '\n') >= 0 } return false } func (cr *chunkedReader) Read(b []uint8) (n int, err error) { for cr.err == nil { if cr.n == 0 { if n > 0 && !cr.chunkHeaderAvailable() { // We've read enough. Don't potentially block // reading a new chunk header. break } cr.beginChunk() continue } if len(b) == 0 { break } rbuf := b if uint64(len(rbuf)) > cr.n { rbuf = rbuf[:cr.n] } var n0 int n0, cr.err = cr.r.Read(rbuf) n += n0 b = b[n0:] cr.n -= uint64(n0) // If we're at the end of a chunk, read the next two // bytes to verify they are "\r\n". if cr.n == 0 && cr.err == nil { if _, cr.err = io.ReadFull(cr.r, cr.buf[:2]); cr.err == nil { if cr.buf[0] != '\r' || cr.buf[1] != '\n' { cr.err = errors.New("malformed chunked encoding") } } } } return n, cr.err } // Read a line of bytes (up to \n) from b. // Give up if the line exceeds maxLineLength. // The returned bytes are a pointer into storage in // the bufio, so they are only valid until the next bufio read. func readLine(b *bufio.Reader) (p []byte, err error) { if p, err = b.ReadSlice('\n'); err != nil { // We always know when EOF is coming. // If the caller asked for a line, there should be a line. if err == io.EOF { err = io.ErrUnexpectedEOF } else if err == bufio.ErrBufferFull { err = ErrLineTooLong } return nil, err } if len(p) >= maxLineLength { return nil, ErrLineTooLong } return trimTrailingWhitespace(p), nil } func trimTrailingWhitespace(b []byte) []byte { for len(b) > 0 && isASCIISpace(b[len(b)-1]) { b = b[:len(b)-1] } return b } func isASCIISpace(b byte) bool { return b == ' ' || b == '\t' || b == '\n' || b == '\r' } // newChunkedWriter returns a new chunkedWriter that translates writes into HTTP // "chunked" format before writing them to w. Closing the returned chunkedWriter // sends the final 0-length chunk that marks the end of the stream. // // newChunkedWriter is not needed by normal applications. The http // package adds chunking automatically if handlers don't set a // Content-Length header. Using newChunkedWriter inside a handler // would result in double chunking or chunking with a Content-Length // length, both of which are wrong. func newChunkedWriter(w io.Writer) io.WriteCloser { return &chunkedWriter{w} } // Writing to chunkedWriter translates to writing in HTTP chunked Transfer // Encoding wire format to the underlying Wire chunkedWriter. type chunkedWriter struct { Wire io.Writer } // Write the contents of data as one chunk to Wire. // NOTE: Note that the corresponding chunk-writing procedure in Conn.Write has // a bug since it does not check for success of io.WriteString func (cw *chunkedWriter) Write(data []byte) (n int, err error) { // Don't send 0-length data. It looks like EOF for chunked encoding. if len(data) == 0 { return 0, nil } if _, err = fmt.Fprintf(cw.Wire, "%x\r\n", len(data)); err != nil { return 0, err } if n, err = cw.Wire.Write(data); err != nil { return } if n != len(data) { err = io.ErrShortWrite return } _, err = io.WriteString(cw.Wire, "\r\n") return } func (cw *chunkedWriter) Close() error { _, err := io.WriteString(cw.Wire, "0\r\n") return err } func parseHexUint(v []byte) (n uint64, err error) { for _, b := range v { n <<= 4 switch { case '0' <= b && b <= '9': b = b - '0' case 'a' <= b && b <= 'f': b = b - 'a' + 10 case 'A' <= b && b <= 'F': b = b - 'A' + 10 default: return 0, errors.New("invalid byte in chunk length") } n |= uint64(b) } return } ubuntu-push-0.68+16.04.20160310.2/http13client/response_test.go0000644000015600001650000003401312670364255024224 0ustar pbuserpbgroup00000000000000// Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "bufio" "bytes" "compress/gzip" "crypto/rand" "fmt" "io" "io/ioutil" "net/http" "net/url" "reflect" "regexp" "strings" "testing" ) type respTest struct { Raw string Resp Response Body string } func dummyReq(method string) *Request { return &Request{Method: method} } func dummyReq11(method string) *Request { return &Request{Method: method, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1} } var respTests = []respTest{ // Unchunked response without Content-Length. { "HTTP/1.0 200 OK\r\n" + "Connection: close\r\n" + "\r\n" + "Body here\n", Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.0", ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), Header: http.Header{ "Connection": {"close"}, // TODO(rsc): Delete? }, Close: true, ContentLength: -1, }, "Body here\n", }, // Unchunked HTTP/1.1 response without Content-Length or // Connection headers. { "HTTP/1.1 200 OK\r\n" + "\r\n" + "Body here\n", Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, Request: dummyReq("GET"), Close: true, ContentLength: -1, }, "Body here\n", }, // Unchunked HTTP/1.1 204 response without Content-Length. { "HTTP/1.1 204 No Content\r\n" + "\r\n" + "Body should not be read!\n", Response{ Status: "204 No Content", StatusCode: 204, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, Request: dummyReq("GET"), Close: false, ContentLength: 0, }, "", }, // Unchunked response with Content-Length. { "HTTP/1.0 200 OK\r\n" + "Content-Length: 10\r\n" + "Connection: close\r\n" + "\r\n" + "Body here\n", Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.0", ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), Header: http.Header{ "Connection": {"close"}, "Content-Length": {"10"}, }, Close: true, ContentLength: 10, }, "Body here\n", }, // Chunked response without Content-Length. { "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0a\r\n" + "Body here\n\r\n" + "09\r\n" + "continued\r\n" + "0\r\n" + "\r\n", Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), Header: http.Header{}, Close: false, ContentLength: -1, TransferEncoding: []string{"chunked"}, }, "Body here\ncontinued", }, // Chunked response with Content-Length. { "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Length: 10\r\n" + "\r\n" + "0a\r\n" + "Body here\n\r\n" + "0\r\n" + "\r\n", Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), Header: http.Header{}, Close: false, ContentLength: -1, TransferEncoding: []string{"chunked"}, }, "Body here\n", }, // Chunked response in response to a HEAD request { "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n", Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("HEAD"), Header: http.Header{}, TransferEncoding: []string{"chunked"}, Close: false, ContentLength: -1, }, "", }, // Content-Length in response to a HEAD request { "HTTP/1.0 200 OK\r\n" + "Content-Length: 256\r\n" + "\r\n", Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.0", ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("HEAD"), Header: http.Header{"Content-Length": {"256"}}, TransferEncoding: nil, Close: true, ContentLength: 256, }, "", }, // Content-Length in response to a HEAD request with HTTP/1.1 { "HTTP/1.1 200 OK\r\n" + "Content-Length: 256\r\n" + "\r\n", Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("HEAD"), Header: http.Header{"Content-Length": {"256"}}, TransferEncoding: nil, Close: false, ContentLength: 256, }, "", }, // No Content-Length or Chunked in response to a HEAD request { "HTTP/1.0 200 OK\r\n" + "\r\n", Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.0", ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("HEAD"), Header: http.Header{}, TransferEncoding: nil, Close: true, ContentLength: -1, }, "", }, // explicit Content-Length of 0. { "HTTP/1.1 200 OK\r\n" + "Content-Length: 0\r\n" + "\r\n", Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), Header: http.Header{ "Content-Length": {"0"}, }, Close: false, ContentLength: 0, }, "", }, // Status line without a Reason-Phrase, but trailing space. // (permitted by RFC 2616) { "HTTP/1.0 303 \r\n\r\n", Response{ Status: "303 ", StatusCode: 303, Proto: "HTTP/1.0", ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), Header: http.Header{}, Close: true, ContentLength: -1, }, "", }, // Status line without a Reason-Phrase, and no trailing space. // (not permitted by RFC 2616, but we'll accept it anyway) { "HTTP/1.0 303\r\n\r\n", Response{ Status: "303 ", StatusCode: 303, Proto: "HTTP/1.0", ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), Header: http.Header{}, Close: true, ContentLength: -1, }, "", }, // golang.org/issue/4767: don't special-case multipart/byteranges responses { `HTTP/1.1 206 Partial Content Connection: close Content-Type: multipart/byteranges; boundary=18a75608c8f47cef some body`, Response{ Status: "206 Partial Content", StatusCode: 206, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), Header: http.Header{ "Content-Type": []string{"multipart/byteranges; boundary=18a75608c8f47cef"}, }, Close: true, ContentLength: -1, }, "some body", }, // Unchunked response without Content-Length, Request is nil { "HTTP/1.0 200 OK\r\n" + "Connection: close\r\n" + "\r\n" + "Body here\n", Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.0", ProtoMajor: 1, ProtoMinor: 0, Header: http.Header{ "Connection": {"close"}, // TODO(rsc): Delete? }, Close: true, ContentLength: -1, }, "Body here\n", }, } func TestReadResponse(t *testing.T) { for i, tt := range respTests { resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request) if err != nil { t.Errorf("#%d: %v", i, err) continue } rbody := resp.Body resp.Body = nil diff(t, fmt.Sprintf("#%d Response", i), resp, &tt.Resp) var bout bytes.Buffer if rbody != nil { _, err = io.Copy(&bout, rbody) if err != nil { t.Errorf("#%d: %v", i, err) continue } rbody.Close() } body := bout.String() if body != tt.Body { t.Errorf("#%d: Body = %q want %q", i, body, tt.Body) } } } func TestWriteResponse(t *testing.T) { for i, tt := range respTests { resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request) if err != nil { t.Errorf("#%d: %v", i, err) continue } err = resp.Write(ioutil.Discard) if err != nil { t.Errorf("#%d: %v", i, err) continue } } } var readResponseCloseInMiddleTests = []struct { chunked, compressed bool }{ {false, false}, {true, false}, {true, true}, } // TestReadResponseCloseInMiddle tests that closing a body after // reading only part of its contents advances the read to the end of // the request, right up until the next request. func TestReadResponseCloseInMiddle(t *testing.T) { for _, test := range readResponseCloseInMiddleTests { fatalf := func(format string, args ...interface{}) { args = append([]interface{}{test.chunked, test.compressed}, args...) t.Fatalf("on test chunked=%v, compressed=%v: "+format, args...) } checkErr := func(err error, msg string) { if err == nil { return } fatalf(msg+": %v", err) } var buf bytes.Buffer buf.WriteString("HTTP/1.1 200 OK\r\n") if test.chunked { buf.WriteString("Transfer-Encoding: chunked\r\n") } else { buf.WriteString("Content-Length: 1000000\r\n") } var wr io.Writer = &buf if test.chunked { wr = newChunkedWriter(wr) } if test.compressed { buf.WriteString("Content-Encoding: gzip\r\n") wr = gzip.NewWriter(wr) } buf.WriteString("\r\n") chunk := bytes.Repeat([]byte{'x'}, 1000) for i := 0; i < 1000; i++ { if test.compressed { // Otherwise this compresses too well. _, err := io.ReadFull(rand.Reader, chunk) checkErr(err, "rand.Reader ReadFull") } wr.Write(chunk) } if test.compressed { err := wr.(*gzip.Writer).Close() checkErr(err, "compressor close") } if test.chunked { buf.WriteString("0\r\n\r\n") } buf.WriteString("Next Request Here") bufr := bufio.NewReader(&buf) resp, err := ReadResponse(bufr, dummyReq("GET")) checkErr(err, "ReadResponse") expectedLength := int64(-1) if !test.chunked { expectedLength = 1000000 } if resp.ContentLength != expectedLength { fatalf("expected response length %d, got %d", expectedLength, resp.ContentLength) } if resp.Body == nil { fatalf("nil body") } if test.compressed { gzReader, err := gzip.NewReader(resp.Body) checkErr(err, "gzip.NewReader") resp.Body = &readerAndCloser{gzReader, resp.Body} } rbuf := make([]byte, 2500) n, err := io.ReadFull(resp.Body, rbuf) checkErr(err, "2500 byte ReadFull") if n != 2500 { fatalf("ReadFull only read %d bytes", n) } if test.compressed == false && !bytes.Equal(bytes.Repeat([]byte{'x'}, 2500), rbuf) { fatalf("ReadFull didn't read 2500 'x'; got %q", string(rbuf)) } resp.Body.Close() rest, err := ioutil.ReadAll(bufr) checkErr(err, "ReadAll on remainder") if e, g := "Next Request Here", string(rest); e != g { g = regexp.MustCompile(`(xx+)`).ReplaceAllStringFunc(g, func(match string) string { return fmt.Sprintf("x(repeated x%d)", len(match)) }) fatalf("remainder = %q, expected %q", g, e) } } } func diff(t *testing.T, prefix string, have, want interface{}) { hv := reflect.ValueOf(have).Elem() wv := reflect.ValueOf(want).Elem() if hv.Type() != wv.Type() { t.Errorf("%s: type mismatch %v want %v", prefix, hv.Type(), wv.Type()) } for i := 0; i < hv.NumField(); i++ { hf := hv.Field(i).Interface() wf := wv.Field(i).Interface() if !reflect.DeepEqual(hf, wf) { t.Errorf("%s: %s = %v want %v", prefix, hv.Type().Field(i).Name, hf, wf) } } } type responseLocationTest struct { location string // Response's Location header or "" requrl string // Response.Request.URL or "" want string wantErr error } var responseLocationTests = []responseLocationTest{ {"/foo", "http://bar.com/baz", "http://bar.com/foo", nil}, {"http://foo.com/", "http://bar.com/baz", "http://foo.com/", nil}, {"", "http://bar.com/baz", "", ErrNoLocation}, } func TestLocationResponse(t *testing.T) { for i, tt := range responseLocationTests { res := new(Response) res.Header = make(http.Header) res.Header.Set("Location", tt.location) if tt.requrl != "" { res.Request = &Request{} var err error res.Request.URL, err = url.Parse(tt.requrl) if err != nil { t.Fatalf("bad test URL %q: %v", tt.requrl, err) } } got, err := res.Location() if tt.wantErr != nil { if err == nil { t.Errorf("%d. err=nil; want %q", i, tt.wantErr) continue } if g, e := err.Error(), tt.wantErr.Error(); g != e { t.Errorf("%d. err=%q; want %q", i, g, e) continue } continue } if err != nil { t.Errorf("%d. err=%q", i, err) continue } if g, e := got.String(), tt.want; g != e { t.Errorf("%d. Location=%q; want %q", i, g, e) } } } func TestResponseStatusStutter(t *testing.T) { r := &Response{ Status: "123 some status", StatusCode: 123, ProtoMajor: 1, ProtoMinor: 3, } var buf bytes.Buffer r.Write(&buf) if strings.Contains(buf.String(), "123 123") { t.Errorf("stutter in status: %s", buf.String()) } } func TestResponseContentLengthShortBody(t *testing.T) { const shortBody = "Short body, not 123 bytes." br := bufio.NewReader(strings.NewReader("HTTP/1.1 200 OK\r\n" + "Content-Length: 123\r\n" + "\r\n" + shortBody)) res, err := ReadResponse(br, &Request{Method: "GET"}) if err != nil { t.Fatal(err) } if res.ContentLength != 123 { t.Fatalf("Content-Length = %d; want 123", res.ContentLength) } var buf bytes.Buffer n, err := io.Copy(&buf, res.Body) if n != int64(len(shortBody)) { t.Errorf("Copied %d bytes; want %d, len(%q)", n, len(shortBody), shortBody) } if buf.String() != shortBody { t.Errorf("Read body %q; want %q", buf.String(), shortBody) } if err != io.ErrUnexpectedEOF { t.Errorf("io.Copy error = %#v; want io.ErrUnexpectedEOF", err) } } func TestReadResponseUnexpectedEOF(t *testing.T) { br := bufio.NewReader(strings.NewReader("HTTP/1.1 301 Moved Permanently\r\n" + "Location: http://example.com")) _, err := ReadResponse(br, nil) if err != io.ErrUnexpectedEOF { t.Errorf("ReadResponse = %v; want io.ErrUnexpectedEOF", err) } } ubuntu-push-0.68+16.04.20160310.2/http13client/header.go0000644000015600001650000000253612670364255022564 0ustar pbuserpbgroup00000000000000// Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "strings" ) // hasToken reports whether token appears with v, ASCII // case-insensitive, with space or comma boundaries. // token must be all lowercase. // v may contain mixed cased. func hasToken(v, token string) bool { if len(token) > len(v) || token == "" { return false } if v == token { return true } for sp := 0; sp <= len(v)-len(token); sp++ { // Check that first character is good. // The token is ASCII, so checking only a single byte // is sufficient. We skip this potential starting // position if both the first byte and its potential // ASCII uppercase equivalent (b|0x20) don't match. // False positives ('^' => '~') are caught by EqualFold. if b := v[sp]; b != token[0] && b|0x20 != token[0] { continue } // Check that start pos is on a valid token boundary. if sp > 0 && !isTokenBoundary(v[sp-1]) { continue } // Check that end pos is on a valid token boundary. if endPos := sp + len(token); endPos != len(v) && !isTokenBoundary(v[endPos]) { continue } if strings.EqualFold(v[sp:sp+len(token)], token) { return true } } return false } func isTokenBoundary(b byte) bool { return b == ' ' || b == ',' || b == '\t' } ubuntu-push-0.68+16.04.20160310.2/http13client/LICENSE0000644000015600001650000000270712670364255022012 0ustar pbuserpbgroup00000000000000Copyright (c) 2012 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ubuntu-push-0.68+16.04.20160310.2/http13client/readrequest_test.go0000644000015600001650000001510712670364255024715 0ustar pbuserpbgroup00000000000000// Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "bufio" "bytes" "fmt" "io" "net/http" "net/url" "reflect" "testing" ) type reqTest struct { Raw string Req *Request Body string Trailer http.Header Error string } var noError = "" var noBody = "" var noTrailer http.Header = nil var reqTests = []reqTest{ // Baseline test; All Request fields included for template use { "GET http://www.techcrunch.com/ HTTP/1.1\r\n" + "Host: www.techcrunch.com\r\n" + "User-Agent: Fake\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "Accept-Language: en-us,en;q=0.5\r\n" + "Accept-Encoding: gzip,deflate\r\n" + "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + "Keep-Alive: 300\r\n" + "Content-Length: 7\r\n" + "Proxy-Connection: keep-alive\r\n\r\n" + "abcdef\n???", &Request{ Method: "GET", URL: &url.URL{ Scheme: "http", Host: "www.techcrunch.com", Path: "/", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, "Accept-Language": {"en-us,en;q=0.5"}, "Accept-Encoding": {"gzip,deflate"}, "Accept-Charset": {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"}, "Keep-Alive": {"300"}, "Proxy-Connection": {"keep-alive"}, "Content-Length": {"7"}, "User-Agent": {"Fake"}, }, Close: false, ContentLength: 7, Host: "www.techcrunch.com", RequestURI: "http://www.techcrunch.com/", }, "abcdef\n", noTrailer, noError, }, // GET request with no body (the normal case) { "GET / HTTP/1.1\r\n" + "Host: foo.com\r\n\r\n", &Request{ Method: "GET", URL: &url.URL{ Path: "/", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, Close: false, ContentLength: 0, Host: "foo.com", RequestURI: "/", }, noBody, noTrailer, noError, }, // Tests that we don't parse a path that looks like a // scheme-relative URI as a scheme-relative URI. { "GET //user@host/is/actually/a/path/ HTTP/1.1\r\n" + "Host: test\r\n\r\n", &Request{ Method: "GET", URL: &url.URL{ Path: "//user@host/is/actually/a/path/", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, Close: false, ContentLength: 0, Host: "test", RequestURI: "//user@host/is/actually/a/path/", }, noBody, noTrailer, noError, }, // Tests a bogus abs_path on the Request-Line (RFC 2616 section 5.1.2) { "GET ../../../../etc/passwd HTTP/1.1\r\n" + "Host: test\r\n\r\n", nil, noBody, noTrailer, "parse ../../../../etc/passwd: invalid URI for request", }, // Tests missing URL: { "GET HTTP/1.1\r\n" + "Host: test\r\n\r\n", nil, noBody, noTrailer, "parse : empty url", }, // Tests chunked body with trailer: { "POST / HTTP/1.1\r\n" + "Host: foo.com\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + "3\r\nfoo\r\n" + "3\r\nbar\r\n" + "0\r\n" + "Trailer-Key: Trailer-Value\r\n" + "\r\n", &Request{ Method: "POST", URL: &url.URL{ Path: "/", }, TransferEncoding: []string{"chunked"}, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, ContentLength: -1, Host: "foo.com", RequestURI: "/", }, "foobar", http.Header{ "Trailer-Key": {"Trailer-Value"}, }, noError, }, // CONNECT request with domain name: { "CONNECT www.google.com:443 HTTP/1.1\r\n\r\n", &Request{ Method: "CONNECT", URL: &url.URL{ Host: "www.google.com:443", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, Close: false, ContentLength: 0, Host: "www.google.com:443", RequestURI: "www.google.com:443", }, noBody, noTrailer, noError, }, // CONNECT request with IP address: { "CONNECT 127.0.0.1:6060 HTTP/1.1\r\n\r\n", &Request{ Method: "CONNECT", URL: &url.URL{ Host: "127.0.0.1:6060", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, Close: false, ContentLength: 0, Host: "127.0.0.1:6060", RequestURI: "127.0.0.1:6060", }, noBody, noTrailer, noError, }, // CONNECT request for RPC: { "CONNECT /_goRPC_ HTTP/1.1\r\n\r\n", &Request{ Method: "CONNECT", URL: &url.URL{ Path: "/_goRPC_", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, Close: false, ContentLength: 0, Host: "", RequestURI: "/_goRPC_", }, noBody, noTrailer, noError, }, // SSDP Notify request. golang.org/issue/3692 { "NOTIFY * HTTP/1.1\r\nServer: foo\r\n\r\n", &Request{ Method: "NOTIFY", URL: &url.URL{ Path: "*", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "Server": []string{"foo"}, }, Close: false, ContentLength: 0, RequestURI: "*", }, noBody, noTrailer, noError, }, // OPTIONS request. Similar to golang.org/issue/3692 { "OPTIONS * HTTP/1.1\r\nServer: foo\r\n\r\n", &Request{ Method: "OPTIONS", URL: &url.URL{ Path: "*", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "Server": []string{"foo"}, }, Close: false, ContentLength: 0, RequestURI: "*", }, noBody, noTrailer, noError, }, } func TestReadRequest(t *testing.T) { for i := range reqTests { tt := &reqTests[i] var braw bytes.Buffer braw.WriteString(tt.Raw) req, err := ReadRequest(bufio.NewReader(&braw)) if err != nil { if err.Error() != tt.Error { t.Errorf("#%d: error %q, want error %q", i, err.Error(), tt.Error) } continue } rbody := req.Body req.Body = nil diff(t, fmt.Sprintf("#%d Request", i), req, tt.Req) var bout bytes.Buffer if rbody != nil { _, err := io.Copy(&bout, rbody) if err != nil { t.Fatalf("#%d. copying body: %v", i, err) } rbody.Close() } body := bout.String() if body != tt.Body { t.Errorf("#%d: Body = %q want %q", i, body, tt.Body) } if !reflect.DeepEqual(tt.Trailer, req.Trailer) { t.Errorf("#%d. Trailers differ.\n got: %v\nwant: %v", i, req.Trailer, tt.Trailer) } } } ubuntu-push-0.68+16.04.20160310.2/http13client/request_test.go0000644000015600001650000004174712670364255024072 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http_test import ( "bufio" "bytes" "fmt" "io" "io/ioutil" . "launchpad.net/ubuntu-push/http13client" "mime/multipart" "net/http" "net/http/httptest" "net/url" "os" "reflect" "regexp" "strings" "testing" ) func TestQuery(t *testing.T) { req := &Request{Method: "GET"} req.URL, _ = url.Parse("http://www.google.com/search?q=foo&q=bar") if q := req.FormValue("q"); q != "foo" { t.Errorf(`req.FormValue("q") = %q, want "foo"`, q) } } func TestPostQuery(t *testing.T) { req, _ := NewRequest("POST", "http://www.google.com/search?q=foo&q=bar&both=x&prio=1&empty=not", strings.NewReader("z=post&both=y&prio=2&empty=")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value") if q := req.FormValue("q"); q != "foo" { t.Errorf(`req.FormValue("q") = %q, want "foo"`, q) } if z := req.FormValue("z"); z != "post" { t.Errorf(`req.FormValue("z") = %q, want "post"`, z) } if bq, found := req.PostForm["q"]; found { t.Errorf(`req.PostForm["q"] = %q, want no entry in map`, bq) } if bz := req.PostFormValue("z"); bz != "post" { t.Errorf(`req.PostFormValue("z") = %q, want "post"`, bz) } if qs := req.Form["q"]; !reflect.DeepEqual(qs, []string{"foo", "bar"}) { t.Errorf(`req.Form["q"] = %q, want ["foo", "bar"]`, qs) } if both := req.Form["both"]; !reflect.DeepEqual(both, []string{"y", "x"}) { t.Errorf(`req.Form["both"] = %q, want ["y", "x"]`, both) } if prio := req.FormValue("prio"); prio != "2" { t.Errorf(`req.FormValue("prio") = %q, want "2" (from body)`, prio) } if empty := req.FormValue("empty"); empty != "" { t.Errorf(`req.FormValue("empty") = %q, want "" (from body)`, empty) } } func TestPatchQuery(t *testing.T) { req, _ := NewRequest("PATCH", "http://www.google.com/search?q=foo&q=bar&both=x&prio=1&empty=not", strings.NewReader("z=post&both=y&prio=2&empty=")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value") if q := req.FormValue("q"); q != "foo" { t.Errorf(`req.FormValue("q") = %q, want "foo"`, q) } if z := req.FormValue("z"); z != "post" { t.Errorf(`req.FormValue("z") = %q, want "post"`, z) } if bq, found := req.PostForm["q"]; found { t.Errorf(`req.PostForm["q"] = %q, want no entry in map`, bq) } if bz := req.PostFormValue("z"); bz != "post" { t.Errorf(`req.PostFormValue("z") = %q, want "post"`, bz) } if qs := req.Form["q"]; !reflect.DeepEqual(qs, []string{"foo", "bar"}) { t.Errorf(`req.Form["q"] = %q, want ["foo", "bar"]`, qs) } if both := req.Form["both"]; !reflect.DeepEqual(both, []string{"y", "x"}) { t.Errorf(`req.Form["both"] = %q, want ["y", "x"]`, both) } if prio := req.FormValue("prio"); prio != "2" { t.Errorf(`req.FormValue("prio") = %q, want "2" (from body)`, prio) } if empty := req.FormValue("empty"); empty != "" { t.Errorf(`req.FormValue("empty") = %q, want "" (from body)`, empty) } } type stringMap map[string][]string type parseContentTypeTest struct { shouldError bool contentType stringMap } var parseContentTypeTests = []parseContentTypeTest{ {false, stringMap{"Content-Type": {"text/plain"}}}, // Empty content type is legal - shoult be treated as // application/octet-stream (RFC 2616, section 7.2.1) {false, stringMap{}}, {true, stringMap{"Content-Type": {"text/plain; boundary="}}}, {false, stringMap{"Content-Type": {"application/unknown"}}}, } func TestParseFormUnknownContentType(t *testing.T) { for i, test := range parseContentTypeTests { req := &Request{ Method: "POST", Header: http.Header(test.contentType), Body: ioutil.NopCloser(strings.NewReader("body")), } err := req.ParseForm() switch { case err == nil && test.shouldError: t.Errorf("test %d should have returned error", i) case err != nil && !test.shouldError: t.Errorf("test %d should not have returned error, got %v", i, err) } } } func TestParseFormInitializeOnError(t *testing.T) { nilBody, _ := NewRequest("POST", "http://www.google.com/search?q=foo", nil) tests := []*Request{ nilBody, {Method: "GET", URL: nil}, } for i, req := range tests { err := req.ParseForm() if req.Form == nil { t.Errorf("%d. Form not initialized, error %v", i, err) } if req.PostForm == nil { t.Errorf("%d. PostForm not initialized, error %v", i, err) } } } func TestMultipartReader(t *testing.T) { req := &Request{ Method: "POST", Header: http.Header{"Content-Type": {`multipart/form-data; boundary="foo123"`}}, Body: ioutil.NopCloser(new(bytes.Buffer)), } multipart, err := req.MultipartReader() if multipart == nil { t.Errorf("expected multipart; error: %v", err) } req.Header = http.Header{"Content-Type": {"text/plain"}} multipart, err = req.MultipartReader() if multipart != nil { t.Error("unexpected multipart for text/plain") } } func TestParseMultipartForm(t *testing.T) { req := &Request{ Method: "POST", Header: http.Header{"Content-Type": {`multipart/form-data; boundary="foo123"`}}, Body: ioutil.NopCloser(new(bytes.Buffer)), } err := req.ParseMultipartForm(25) if err == nil { t.Error("expected multipart EOF, got nil") } req.Header = http.Header{"Content-Type": {"text/plain"}} err = req.ParseMultipartForm(25) if err != ErrNotMultipart { t.Error("expected ErrNotMultipart for text/plain") } } func TestRedirect(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": w.Header().Set("Location", "/foo/") w.WriteHeader(http.StatusSeeOther) case "/foo/": fmt.Fprintf(w, "foo") default: w.WriteHeader(http.StatusBadRequest) } })) defer ts.Close() var end = regexp.MustCompile("/foo/$") r, err := Get(ts.URL) if err != nil { t.Fatal(err) } r.Body.Close() url := r.Request.URL.String() if r.StatusCode != 200 || !end.MatchString(url) { t.Fatalf("Get got status %d at %q, want 200 matching /foo/$", r.StatusCode, url) } } func TestSetBasicAuth(t *testing.T) { r, _ := NewRequest("GET", "http://example.com/", nil) r.SetBasicAuth("Aladdin", "open sesame") if g, e := r.Header.Get("Authorization"), "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="; g != e { t.Errorf("got header %q, want %q", g, e) } } func TestMultipartRequest(t *testing.T) { // Test that we can read the values and files of a // multipart request with FormValue and FormFile, // and that ParseMultipartForm can be called multiple times. req := newTestMultipartRequest(t) if err := req.ParseMultipartForm(25); err != nil { t.Fatal("ParseMultipartForm first call:", err) } defer req.MultipartForm.RemoveAll() validateTestMultipartContents(t, req, false) if err := req.ParseMultipartForm(25); err != nil { t.Fatal("ParseMultipartForm second call:", err) } validateTestMultipartContents(t, req, false) } func TestMultipartRequestAuto(t *testing.T) { // Test that FormValue and FormFile automatically invoke // ParseMultipartForm and return the right values. req := newTestMultipartRequest(t) defer func() { if req.MultipartForm != nil { req.MultipartForm.RemoveAll() } }() validateTestMultipartContents(t, req, true) } func TestMissingFileMultipartRequest(t *testing.T) { // Test that FormFile returns an error if // the named file is missing. req := newTestMultipartRequest(t) testMissingFile(t, req) } // Test that FormValue invokes ParseMultipartForm. func TestFormValueCallsParseMultipartForm(t *testing.T) { req, _ := NewRequest("POST", "http://www.google.com/", strings.NewReader("z=post")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value") if req.Form != nil { t.Fatal("Unexpected request Form, want nil") } req.FormValue("z") if req.Form == nil { t.Fatal("ParseMultipartForm not called by FormValue") } } // Test that FormFile invokes ParseMultipartForm. func TestFormFileCallsParseMultipartForm(t *testing.T) { req := newTestMultipartRequest(t) if req.Form != nil { t.Fatal("Unexpected request Form, want nil") } req.FormFile("") if req.Form == nil { t.Fatal("ParseMultipartForm not called by FormFile") } } // Test that ParseMultipartForm errors if called // after MultipartReader on the same request. func TestParseMultipartFormOrder(t *testing.T) { req := newTestMultipartRequest(t) if _, err := req.MultipartReader(); err != nil { t.Fatalf("MultipartReader: %v", err) } if err := req.ParseMultipartForm(1024); err == nil { t.Fatal("expected an error from ParseMultipartForm after call to MultipartReader") } } // Test that MultipartReader errors if called // after ParseMultipartForm on the same request. func TestMultipartReaderOrder(t *testing.T) { req := newTestMultipartRequest(t) if err := req.ParseMultipartForm(25); err != nil { t.Fatalf("ParseMultipartForm: %v", err) } defer req.MultipartForm.RemoveAll() if _, err := req.MultipartReader(); err == nil { t.Fatal("expected an error from MultipartReader after call to ParseMultipartForm") } } // Test that FormFile errors if called after // MultipartReader on the same request. func TestFormFileOrder(t *testing.T) { req := newTestMultipartRequest(t) if _, err := req.MultipartReader(); err != nil { t.Fatalf("MultipartReader: %v", err) } if _, _, err := req.FormFile(""); err == nil { t.Fatal("expected an error from FormFile after call to MultipartReader") } } var readRequestErrorTests = []struct { in string err error }{ {"GET / HTTP/1.1\r\nheader:foo\r\n\r\n", nil}, {"GET / HTTP/1.1\r\nheader:foo\r\n", io.ErrUnexpectedEOF}, {"", io.EOF}, } func TestReadRequestErrors(t *testing.T) { for i, tt := range readRequestErrorTests { _, err := ReadRequest(bufio.NewReader(strings.NewReader(tt.in))) if err != tt.err { t.Errorf("%d. got error = %v; want %v", i, err, tt.err) } } } func TestNewRequestHost(t *testing.T) { req, err := NewRequest("GET", "http://localhost:1234/", nil) if err != nil { t.Fatal(err) } if req.Host != "localhost:1234" { t.Errorf("Host = %q; want localhost:1234", req.Host) } } func TestNewRequestContentLength(t *testing.T) { readByte := func(r io.Reader) io.Reader { var b [1]byte r.Read(b[:]) return r } tests := []struct { r io.Reader want int64 }{ {bytes.NewReader([]byte("123")), 3}, {bytes.NewBuffer([]byte("1234")), 4}, {strings.NewReader("12345"), 5}, // Not detected: {struct{ io.Reader }{strings.NewReader("xyz")}, 0}, {io.NewSectionReader(strings.NewReader("x"), 0, 6), 0}, {readByte(io.NewSectionReader(strings.NewReader("xy"), 0, 6)), 0}, } for _, tt := range tests { req, err := NewRequest("POST", "http://localhost/", tt.r) if err != nil { t.Fatal(err) } if req.ContentLength != tt.want { t.Errorf("ContentLength(%T) = %d; want %d", tt.r, req.ContentLength, tt.want) } } } var parseHTTPVersionTests = []struct { vers string major, minor int ok bool }{ {"HTTP/0.9", 0, 9, true}, {"HTTP/1.0", 1, 0, true}, {"HTTP/1.1", 1, 1, true}, {"HTTP/3.14", 3, 14, true}, {"HTTP", 0, 0, false}, {"HTTP/one.one", 0, 0, false}, {"HTTP/1.1/", 0, 0, false}, {"HTTP/-1,0", 0, 0, false}, {"HTTP/0,-1", 0, 0, false}, {"HTTP/", 0, 0, false}, {"HTTP/1,1", 0, 0, false}, } func TestParseHTTPVersion(t *testing.T) { for _, tt := range parseHTTPVersionTests { major, minor, ok := ParseHTTPVersion(tt.vers) if ok != tt.ok || major != tt.major || minor != tt.minor { type version struct { major, minor int ok bool } t.Errorf("failed to parse %q, expected: %#v, got %#v", tt.vers, version{tt.major, tt.minor, tt.ok}, version{major, minor, ok}) } } } type logWrites struct { t *testing.T dst *[]string } func (l logWrites) WriteByte(c byte) error { l.t.Fatalf("unexpected WriteByte call") return nil } func (l logWrites) Write(p []byte) (n int, err error) { *l.dst = append(*l.dst, string(p)) return len(p), nil } func TestRequestWriteBufferedWriter(t *testing.T) { got := []string{} req, _ := NewRequest("GET", "http://foo.com/", nil) req.Write(logWrites{t, &got}) want := []string{ "GET / HTTP/1.1\r\n", "Host: foo.com\r\n", "User-Agent: " + DefaultUserAgent + "\r\n", "\r\n", } if !reflect.DeepEqual(got, want) { t.Errorf("Writes = %q\n Want = %q", got, want) } } func testMissingFile(t *testing.T, req *Request) { f, fh, err := req.FormFile("missing") if f != nil { t.Errorf("FormFile file = %v, want nil", f) } if fh != nil { t.Errorf("FormFile file header = %q, want nil", fh) } if err != ErrMissingFile { t.Errorf("FormFile err = %q, want ErrMissingFile", err) } } func newTestMultipartRequest(t *testing.T) *Request { b := strings.NewReader(strings.Replace(message, "\n", "\r\n", -1)) req, err := NewRequest("POST", "/", b) if err != nil { t.Fatal("NewRequest:", err) } ctype := fmt.Sprintf(`multipart/form-data; boundary="%s"`, boundary) req.Header.Set("Content-type", ctype) return req } func validateTestMultipartContents(t *testing.T, req *Request, allMem bool) { if g, e := req.FormValue("texta"), textaValue; g != e { t.Errorf("texta value = %q, want %q", g, e) } if g, e := req.FormValue("textb"), textbValue; g != e { t.Errorf("textb value = %q, want %q", g, e) } if g := req.FormValue("missing"); g != "" { t.Errorf("missing value = %q, want empty string", g) } assertMem := func(n string, fd multipart.File) { if _, ok := fd.(*os.File); ok { t.Error(n, " is *os.File, should not be") } } fda := testMultipartFile(t, req, "filea", "filea.txt", fileaContents) defer fda.Close() assertMem("filea", fda) fdb := testMultipartFile(t, req, "fileb", "fileb.txt", filebContents) defer fdb.Close() if allMem { assertMem("fileb", fdb) } else { if _, ok := fdb.(*os.File); !ok { t.Errorf("fileb has unexpected underlying type %T", fdb) } } testMissingFile(t, req) } func testMultipartFile(t *testing.T, req *Request, key, expectFilename, expectContent string) multipart.File { f, fh, err := req.FormFile(key) if err != nil { t.Fatalf("FormFile(%q): %q", key, err) } if fh.Filename != expectFilename { t.Errorf("filename = %q, want %q", fh.Filename, expectFilename) } var b bytes.Buffer _, err = io.Copy(&b, f) if err != nil { t.Fatal("copying contents:", err) } if g := b.String(); g != expectContent { t.Errorf("contents = %q, want %q", g, expectContent) } return f } const ( fileaContents = "This is a test file." filebContents = "Another test file." textaValue = "foo" textbValue = "bar" boundary = `MyBoundary` ) const message = ` --MyBoundary Content-Disposition: form-data; name="filea"; filename="filea.txt" Content-Type: text/plain ` + fileaContents + ` --MyBoundary Content-Disposition: form-data; name="fileb"; filename="fileb.txt" Content-Type: text/plain ` + filebContents + ` --MyBoundary Content-Disposition: form-data; name="texta" ` + textaValue + ` --MyBoundary Content-Disposition: form-data; name="textb" ` + textbValue + ` --MyBoundary-- ` func benchmarkReadRequest(b *testing.B, request string) { request = request + "\n" // final \n request = strings.Replace(request, "\n", "\r\n", -1) // expand \n to \r\n b.SetBytes(int64(len(request))) r := bufio.NewReader(&infiniteReader{buf: []byte(request)}) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _, err := ReadRequest(r) if err != nil { b.Fatalf("failed to read request: %v", err) } } } // infiniteReader satisfies Read requests as if the contents of buf // loop indefinitely. type infiniteReader struct { buf []byte offset int } func (r *infiniteReader) Read(b []byte) (int, error) { n := copy(b, r.buf[r.offset:]) r.offset = (r.offset + n) % len(r.buf) return n, nil } func BenchmarkReadRequestChrome(b *testing.B) { // https://github.com/felixge/node-http-perf/blob/master/fixtures/get.http benchmarkReadRequest(b, `GET / HTTP/1.1 Host: localhost:8080 Connection: keep-alive Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.52 Safari/537.17 Accept-Encoding: gzip,deflate,sdch Accept-Language: en-US,en;q=0.8 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3 Cookie: __utma=1.1978842379.1323102373.1323102373.1323102373.1; EPi:NumberOfVisits=1,2012-02-28T13:42:18; CrmSession=5b707226b9563e1bc69084d07a107c98; plushContainerWidth=100%25; plushNoTopMenu=0; hudson_auto_refresh=false `) } func BenchmarkReadRequestCurl(b *testing.B) { // curl http://localhost:8080/ benchmarkReadRequest(b, `GET / HTTP/1.1 User-Agent: curl/7.27.0 Host: localhost:8080 Accept: */* `) } func BenchmarkReadRequestApachebench(b *testing.B) { // ab -n 1 -c 1 http://localhost:8080/ benchmarkReadRequest(b, `GET / HTTP/1.0 Host: localhost:8080 User-Agent: ApacheBench/2.3 Accept: */* `) } func BenchmarkReadRequestSiege(b *testing.B) { // siege -r 1 -c 1 http://localhost:8080/ benchmarkReadRequest(b, `GET / HTTP/1.1 Host: localhost:8080 Accept: */* Accept-Encoding: gzip User-Agent: JoeDog/1.00 [en] (X11; I; Siege 2.70) Connection: keep-alive `) } func BenchmarkReadRequestWrk(b *testing.B) { // wrk -t 1 -r 1 -c 1 http://localhost:8080/ benchmarkReadRequest(b, `GET / HTTP/1.1 Host: localhost:8080 `) } ubuntu-push-0.68+16.04.20160310.2/http13client/response.go0000644000015600001650000001764612670364255023202 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // HTTP Response reading and parsing. package http import ( "bufio" "bytes" "crypto/tls" "errors" "io" "net/http" "net/textproto" "net/url" "strconv" "strings" ) var respExcludeHeader = map[string]bool{ "Content-Length": true, "Transfer-Encoding": true, "Trailer": true, } // Response represents the response from an HTTP request. // type Response struct { Status string // e.g. "200 OK" StatusCode int // e.g. 200 Proto string // e.g. "HTTP/1.0" ProtoMajor int // e.g. 1 ProtoMinor int // e.g. 0 // Header maps header keys to values. If the response had multiple // headers with the same key, they may be concatenated, with comma // delimiters. (Section 4.2 of RFC 2616 requires that multiple headers // be semantically equivalent to a comma-delimited sequence.) Values // duplicated by other fields in this struct (e.g., ContentLength) are // omitted from Header. // // Keys in the map are canonicalized (see CanonicalHeaderKey). Header http.Header // Body represents the response body. // // The http Client and Transport guarantee that Body is always // non-nil, even on responses without a body or responses with // a zero-length body. It is the caller's responsibility to // close Body. // // The Body is automatically dechunked if the server replied // with a "chunked" Transfer-Encoding. Body io.ReadCloser // ContentLength records the length of the associated content. The // value -1 indicates that the length is unknown. Unless Request.Method // is "HEAD", values >= 0 indicate that the given number of bytes may // be read from Body. ContentLength int64 // Contains transfer encodings from outer-most to inner-most. Value is // nil, means that "identity" encoding is used. TransferEncoding []string // Close records whether the header directed that the connection be // closed after reading Body. The value is advice for clients: neither // ReadResponse nor Response.Write ever closes a connection. Close bool // Trailer maps trailer keys to values, in the same // format as the header. Trailer http.Header // The Request that was sent to obtain this Response. // Request's Body is nil (having already been consumed). // This is only populated for Client requests. Request *Request // TLS contains information about the TLS connection on which the // response was received. It is nil for unencrypted responses. // The pointer is shared between responses and should not be // modified. TLS *tls.ConnectionState } // Cookies parses and returns the cookies set in the Set-Cookie headers. func (r *Response) Cookies() []*http.Cookie { return readSetCookies(r.Header) } var ErrNoLocation = errors.New("http: no Location header in response") // Location returns the URL of the response's "Location" header, // if present. Relative redirects are resolved relative to // the Response's Request. ErrNoLocation is returned if no // Location header is present. func (r *Response) Location() (*url.URL, error) { lv := r.Header.Get("Location") if lv == "" { return nil, ErrNoLocation } if r.Request != nil && r.Request.URL != nil { return r.Request.URL.Parse(lv) } return url.Parse(lv) } // ReadResponse reads and returns an HTTP response from r. // The req parameter optionally specifies the Request that corresponds // to this Response. If nil, a GET request is assumed. // Clients must call resp.Body.Close when finished reading resp.Body. // After that call, clients can inspect resp.Trailer to find key/value // pairs included in the response trailer. func ReadResponse(r *bufio.Reader, req *Request) (*Response, error) { tp := textproto.NewReader(r) resp := &Response{ Request: req, } // Parse the first line of the response. line, err := tp.ReadLine() if err != nil { if err == io.EOF { err = io.ErrUnexpectedEOF } return nil, err } f := strings.SplitN(line, " ", 3) if len(f) < 2 { return nil, &badStringError{"malformed HTTP response", line} } reasonPhrase := "" if len(f) > 2 { reasonPhrase = f[2] } resp.Status = f[1] + " " + reasonPhrase resp.StatusCode, err = strconv.Atoi(f[1]) if err != nil { return nil, &badStringError{"malformed HTTP status code", f[1]} } resp.Proto = f[0] var ok bool if resp.ProtoMajor, resp.ProtoMinor, ok = ParseHTTPVersion(resp.Proto); !ok { return nil, &badStringError{"malformed HTTP version", resp.Proto} } // Parse the response headers. mimeHeader, err := tp.ReadMIMEHeader() if err != nil { if err == io.EOF { err = io.ErrUnexpectedEOF } return nil, err } resp.Header = http.Header(mimeHeader) fixPragmaCacheControl(resp.Header) err = readTransfer(resp, r) if err != nil { return nil, err } return resp, nil } // RFC2616: Should treat // Pragma: no-cache // like // Cache-Control: no-cache func fixPragmaCacheControl(header http.Header) { if hp, ok := header["Pragma"]; ok && len(hp) > 0 && hp[0] == "no-cache" { if _, presentcc := header["Cache-Control"]; !presentcc { header["Cache-Control"] = []string{"no-cache"} } } } // ProtoAtLeast reports whether the HTTP protocol used // in the response is at least major.minor. func (r *Response) ProtoAtLeast(major, minor int) bool { return r.ProtoMajor > major || r.ProtoMajor == major && r.ProtoMinor >= minor } // Writes the response (header, body and trailer) in wire format. This method // consults the following fields of the response: // // StatusCode // ProtoMajor // ProtoMinor // Request.Method // TransferEncoding // Trailer // Body // ContentLength // Header, values for non-canonical keys will have unpredictable behavior // // Body is closed after it is sent. func (r *Response) Write(w io.Writer) error { // Status line text := r.Status if text == "" { text = http.StatusText(r.StatusCode) if text == "" { text = "status code " + strconv.Itoa(r.StatusCode) } } protoMajor, protoMinor := strconv.Itoa(r.ProtoMajor), strconv.Itoa(r.ProtoMinor) statusCode := strconv.Itoa(r.StatusCode) + " " text = strings.TrimPrefix(text, statusCode) if _, err := io.WriteString(w, "HTTP/"+protoMajor+"."+protoMinor+" "+statusCode+text+"\r\n"); err != nil { return err } // Clone it, so we can modify r1 as needed. r1 := new(Response) *r1 = *r if r1.ContentLength == 0 && r1.Body != nil { // Is it actually 0 length? Or just unknown? var buf [1]byte n, err := r1.Body.Read(buf[:]) if err != nil && err != io.EOF { return err } if n == 0 { // Reset it to a known zero reader, in case underlying one // is unhappy being read repeatedly. r1.Body = eofReader } else { r1.ContentLength = -1 r1.Body = struct { io.Reader io.Closer }{ io.MultiReader(bytes.NewReader(buf[:1]), r.Body), r.Body, } } } // If we're sending a non-chunked HTTP/1.1 response without a // content-length, the only way to do that is the old HTTP/1.0 // way, by noting the EOF with a connection close, so we need // to set Close. if r1.ContentLength == -1 && !r1.Close && r1.ProtoAtLeast(1, 1) && !chunked(r1.TransferEncoding) { r1.Close = true } // Process Body,ContentLength,Close,Trailer tw, err := newTransferWriter(r1) if err != nil { return err } err = tw.WriteHeader(w) if err != nil { return err } // Rest of header err = r.Header.WriteSubset(w, respExcludeHeader) if err != nil { return err } // contentLengthAlreadySent may have been already sent for // POST/PUT requests, even if zero length. See Issue 8180. contentLengthAlreadySent := tw.shouldSendContentLength() if r1.ContentLength == 0 && !chunked(r1.TransferEncoding) && !contentLengthAlreadySent { if _, err := io.WriteString(w, "Content-Length: 0\r\n"); err != nil { return err } } // End-of-header if _, err := io.WriteString(w, "\r\n"); err != nil { return err } // Write body and trailer err = tw.WriteBody(w) if err != nil { return err } // Success return nil } ubuntu-push-0.68+16.04.20160310.2/http13client/lex.go0000644000015600001650000000257312670364255022125 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http // This file deals with lexical matters of HTTP var isTokenTable = [127]bool{ '!': true, '#': true, '$': true, '%': true, '&': true, '\'': true, '*': true, '+': true, '-': true, '.': true, '0': true, '1': true, '2': true, '3': true, '4': true, '5': true, '6': true, '7': true, '8': true, '9': true, 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true, 'S': true, 'T': true, 'U': true, 'W': true, 'V': true, 'X': true, 'Y': true, 'Z': true, '^': true, '_': true, '`': true, 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true, 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true, 'y': true, 'z': true, '|': true, '~': true, } func isToken(r rune) bool { i := int(r) return i < len(isTokenTable) && isTokenTable[i] } func isNotToken(r rune) bool { return !isToken(r) } ubuntu-push-0.68+16.04.20160310.2/http13client/cookie.go0000644000015600001650000001532112670364255022601 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "log" "net" "net/http" "strconv" "strings" "time" ) // This implementation is done according to RFC 6265: // // http://tools.ietf.org/html/rfc6265 // readSetCookies parses all "Set-Cookie" values from // the header h and returns the successfully parsed Cookies. func readSetCookies(h http.Header) []*http.Cookie { cookies := []*http.Cookie{} for _, line := range h["Set-Cookie"] { parts := strings.Split(strings.TrimSpace(line), ";") if len(parts) == 1 && parts[0] == "" { continue } parts[0] = strings.TrimSpace(parts[0]) j := strings.Index(parts[0], "=") if j < 0 { continue } name, value := parts[0][:j], parts[0][j+1:] if !isCookieNameValid(name) { continue } value, success := parseCookieValue(value) if !success { continue } c := &http.Cookie{ Name: name, Value: value, Raw: line, } for i := 1; i < len(parts); i++ { parts[i] = strings.TrimSpace(parts[i]) if len(parts[i]) == 0 { continue } attr, val := parts[i], "" if j := strings.Index(attr, "="); j >= 0 { attr, val = attr[:j], attr[j+1:] } lowerAttr := strings.ToLower(attr) val, success = parseCookieValue(val) if !success { c.Unparsed = append(c.Unparsed, parts[i]) continue } switch lowerAttr { case "secure": c.Secure = true continue case "httponly": c.HttpOnly = true continue case "domain": c.Domain = val continue case "max-age": secs, err := strconv.Atoi(val) if err != nil || secs != 0 && val[0] == '0' { break } if secs <= 0 { c.MaxAge = -1 } else { c.MaxAge = secs } continue case "expires": c.RawExpires = val exptime, err := time.Parse(time.RFC1123, val) if err != nil { exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val) if err != nil { c.Expires = time.Time{} break } } c.Expires = exptime.UTC() continue case "path": c.Path = val continue } c.Unparsed = append(c.Unparsed, parts[i]) } cookies = append(cookies, c) } return cookies } // readCookies parses all "Cookie" values from the header h and // returns the successfully parsed Cookies. // // if filter isn't empty, only cookies of that name are returned func readCookies(h http.Header, filter string) []*http.Cookie { cookies := []*http.Cookie{} lines, ok := h["Cookie"] if !ok { return cookies } for _, line := range lines { parts := strings.Split(strings.TrimSpace(line), ";") if len(parts) == 1 && parts[0] == "" { continue } // Per-line attributes parsedPairs := 0 for i := 0; i < len(parts); i++ { parts[i] = strings.TrimSpace(parts[i]) if len(parts[i]) == 0 { continue } name, val := parts[i], "" if j := strings.Index(name, "="); j >= 0 { name, val = name[:j], name[j+1:] } if !isCookieNameValid(name) { continue } if filter != "" && filter != name { continue } val, success := parseCookieValue(val) if !success { continue } cookies = append(cookies, &http.Cookie{Name: name, Value: val}) parsedPairs++ } } return cookies } // validCookieDomain returns wheter v is a valid cookie domain-value. func validCookieDomain(v string) bool { if isCookieDomainName(v) { return true } if net.ParseIP(v) != nil && !strings.Contains(v, ":") { return true } return false } // isCookieDomainName returns whether s is a valid domain name or a valid // domain name with a leading dot '.'. It is almost a direct copy of // package net's isDomainName. func isCookieDomainName(s string) bool { if len(s) == 0 { return false } if len(s) > 255 { return false } if s[0] == '.' { // A cookie a domain attribute may start with a leading dot. s = s[1:] } last := byte('.') ok := false // Ok once we've seen a letter. partlen := 0 for i := 0; i < len(s); i++ { c := s[i] switch { default: return false case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': // No '_' allowed here (in contrast to package net). ok = true partlen++ case '0' <= c && c <= '9': // fine partlen++ case c == '-': // Byte before dash cannot be dot. if last == '.' { return false } partlen++ case c == '.': // Byte before dot cannot be dot, dash. if last == '.' || last == '-' { return false } if partlen > 63 || partlen == 0 { return false } partlen = 0 } last = c } if last == '-' || partlen > 63 { return false } return ok } var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-") func sanitizeCookieName(n string) string { return cookieNameSanitizer.Replace(n) } // http://tools.ietf.org/html/rfc6265#section-4.1.1 // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E // ; US-ASCII characters excluding CTLs, // ; whitespace DQUOTE, comma, semicolon, // ; and backslash // We loosen this as spaces and commas are common in cookie values // but we produce a quoted cookie-value in when value starts or ends // with a comma or space. // See http://golang.org/issue/7243 for the discussion. func sanitizeCookieValue(v string) string { v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v) if len(v) == 0 { return v } if v[0] == ' ' || v[0] == ',' || v[len(v)-1] == ' ' || v[len(v)-1] == ',' { return `"` + v + `"` } return v } func validCookieValueByte(b byte) bool { return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\' } // path-av = "Path=" path-value // path-value = func sanitizeCookiePath(v string) string { return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v) } func validCookiePathByte(b byte) bool { return 0x20 <= b && b < 0x7f && b != ';' } func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string { ok := true for i := 0; i < len(v); i++ { if valid(v[i]) { continue } log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName) ok = false break } if ok { return v } buf := make([]byte, 0, len(v)) for i := 0; i < len(v); i++ { if b := v[i]; valid(b) { buf = append(buf, b) } } return string(buf) } func parseCookieValue(raw string) (string, bool) { // Strip the quotes, if present. if len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' { raw = raw[1 : len(raw)-1] } for i := 0; i < len(raw); i++ { if !validCookieValueByte(raw[i]) { return "", false } } return raw, true } func isCookieNameValid(raw string) bool { return strings.IndexFunc(raw, isNotToken) < 0 } ubuntu-push-0.68+16.04.20160310.2/http13client/lex_test.go0000644000015600001650000000127412670364255023161 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "testing" ) func isChar(c rune) bool { return c <= 127 } func isCtl(c rune) bool { return c <= 31 || c == 127 } func isSeparator(c rune) bool { switch c { case '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t': return true } return false } func TestIsToken(t *testing.T) { for i := 0; i <= 130; i++ { r := rune(i) expected := isChar(r) && !isCtl(r) && !isSeparator(r) if isToken(r) != expected { t.Errorf("isToken(0x%x) = %v", r, !expected) } } } ubuntu-push-0.68+16.04.20160310.2/http13client/proxy_test.go0000644000015600001650000000413212670364255023546 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "net/url" "os" "testing" ) // TODO(mattn): // test ProxyAuth var UseProxyTests = []struct { host string match bool }{ // Never proxy localhost: {"localhost:80", false}, {"127.0.0.1", false}, {"127.0.0.2", false}, {"[::1]", false}, {"[::2]", true}, // not a loopback address {"barbaz.net", false}, // match as .barbaz.net {"foobar.com", false}, // have a port but match {"foofoobar.com", true}, // not match as a part of foobar.com {"baz.com", true}, // not match as a part of barbaz.com {"localhost.net", true}, // not match as suffix of address {"local.localhost", true}, // not match as prefix as address {"barbarbaz.net", true}, // not match because NO_PROXY have a '.' {"www.foobar.com", false}, // match because NO_PROXY includes "foobar.com" } func TestUseProxy(t *testing.T) { ResetProxyEnv() os.Setenv("NO_PROXY", "foobar.com, .barbaz.net") for _, test := range UseProxyTests { if useProxy(test.host+":80") != test.match { t.Errorf("useProxy(%v) = %v, want %v", test.host, !test.match, test.match) } } } var cacheKeysTests = []struct { proxy string scheme string addr string key string }{ {"", "http", "foo.com", "|http|foo.com"}, {"", "https", "foo.com", "|https|foo.com"}, {"http://foo.com", "http", "foo.com", "http://foo.com|http|"}, {"http://foo.com", "https", "foo.com", "http://foo.com|https|foo.com"}, } func TestCacheKeys(t *testing.T) { for _, tt := range cacheKeysTests { var proxy *url.URL if tt.proxy != "" { u, err := url.Parse(tt.proxy) if err != nil { t.Fatal(err) } proxy = u } cm := connectMethod{proxy, tt.scheme, tt.addr} if got := cm.key().String(); got != tt.key { t.Fatalf("{%q, %q, %q} cache key = %q; want %q", tt.proxy, tt.scheme, tt.addr, got, tt.key) } } } func ResetProxyEnv() { for _, v := range []string{"HTTP_PROXY", "http_proxy", "NO_PROXY", "no_proxy"} { os.Setenv(v, "") } ResetCachedEnvironment() } ubuntu-push-0.68+16.04.20160310.2/http13client/server.go0000644000015600001650000000331312670364255022634 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "fmt" "io" "io/ioutil" "log" "net" "sync" ) type eofReaderWithWriteTo struct{} func (eofReaderWithWriteTo) WriteTo(io.Writer) (int64, error) { return 0, nil } func (eofReaderWithWriteTo) Read([]byte) (int, error) { return 0, io.EOF } // eofReader is a non-nil io.ReadCloser that always returns EOF. // It has a WriteTo method so io.Copy won't need a buffer. var eofReader = &struct { eofReaderWithWriteTo io.Closer }{ eofReaderWithWriteTo{}, ioutil.NopCloser(nil), } // Verify that an io.Copy from an eofReader won't require a buffer. var _ io.WriterTo = eofReader // loggingConn is used for debugging. type loggingConn struct { name string net.Conn } var ( uniqNameMu sync.Mutex uniqNameNext = make(map[string]int) ) func newLoggingConn(baseName string, c net.Conn) net.Conn { uniqNameMu.Lock() defer uniqNameMu.Unlock() uniqNameNext[baseName]++ return &loggingConn{ name: fmt.Sprintf("%s-%d", baseName, uniqNameNext[baseName]), Conn: c, } } func (c *loggingConn) Write(p []byte) (n int, err error) { log.Printf("%s.Write(%d) = ....", c.name, len(p)) n, err = c.Conn.Write(p) log.Printf("%s.Write(%d) = %d, %v", c.name, len(p), n, err) return } func (c *loggingConn) Read(p []byte) (n int, err error) { log.Printf("%s.Read(%d) = ....", c.name, len(p)) n, err = c.Conn.Read(p) log.Printf("%s.Read(%d) = %d, %v", c.name, len(p), n, err) return } func (c *loggingConn) Close() (err error) { log.Printf("%s.Close() = ...", c.name) err = c.Conn.Close() log.Printf("%s.Close() = %v", c.name, err) return } ubuntu-push-0.68+16.04.20160310.2/http13client/_patches/0000755000015600001650000000000012670364532022563 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/http13client/_patches/tweak_doc_go.patch0000644000015600001650000000244612670364255026241 0ustar pbuserpbgroup00000000000000=== modified file 'http13client/doc.go' --- http13client/doc.go 2014-03-19 20:20:19 +0000 +++ http13client/doc.go 2014-03-20 12:11:42 +0000 @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. /* -Package http provides HTTP client and server implementations. +Package http contains the client subset of go 1.3 development net/http. Get, Head, Post, and PostForm make HTTP (or HTTPS) requests: @@ -52,29 +52,5 @@ Clients and Transports are safe for concurrent use by multiple goroutines and for efficiency should only be created once and re-used. - -ListenAndServe starts an HTTP server with a given address and handler. -The handler is usually nil, which means to use DefaultServeMux. -Handle and HandleFunc add handlers to DefaultServeMux: - - http.Handle("/foo", fooHandler) - - http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) - }) - - log.Fatal(http.ListenAndServe(":8080", nil)) - -More control over the server's behavior is available by creating a -custom Server: - - s := &http.Server{ - Addr: ":8080", - Handler: myHandler, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - } - log.Fatal(s.ListenAndServe()) */ package http ubuntu-push-0.68+16.04.20160310.2/http13client/_patches/empty_server.patch0000644000015600001650000042036412670364255026343 0ustar pbuserpbgroup00000000000000=== modified file 'http13client/serve_test.go' --- http13client/serve_test.go 2014-06-20 11:00:47 +0000 +++ http13client/serve_test.go 2014-06-20 12:00:22 +0000 @@ -2,60 +2,15 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// End-to-end serving tests - package http_test import ( - "bufio" - "bytes" - "crypto/tls" - "errors" - "fmt" "io" - "io/ioutil" - "log" "net" - . "launchpad.net/ubuntu-push/http13client" - "net/http/httptest" - "net/http/httputil" - "net/url" - "os" - "os/exec" - "reflect" - "runtime" - "strconv" - "strings" - "sync" - "sync/atomic" - "syscall" - "testing" "time" ) type dummyAddr string -type oneConnListener struct { - conn net.Conn -} - -func (l *oneConnListener) Accept() (c net.Conn, err error) { - c = l.conn - if c == nil { - err = io.EOF - return - } - err = nil - l.conn = nil - return -} - -func (l *oneConnListener) Close() error { - return nil -} - -func (l *oneConnListener) Addr() net.Addr { - return dummyAddr("test-address") -} func (a dummyAddr) Network() string { return string(a) @@ -93,1325 +48,6 @@ return nil } -type testConn struct { - readBuf bytes.Buffer - writeBuf bytes.Buffer - closec chan bool // if non-nil, send value to it on close - noopConn -} - -func (c *testConn) Read(b []byte) (int, error) { - return c.readBuf.Read(b) -} - -func (c *testConn) Write(b []byte) (int, error) { - return c.writeBuf.Write(b) -} - -func (c *testConn) Close() error { - select { - case c.closec <- true: - default: - } - return nil -} - -// reqBytes treats req as a request (with \n delimiters) and returns it with \r\n delimiters, -// ending in \r\n\r\n -func reqBytes(req string) []byte { - return []byte(strings.Replace(strings.TrimSpace(req), "\n", "\r\n", -1) + "\r\n\r\n") -} - -type handlerTest struct { - handler Handler -} - -func newHandlerTest(h Handler) handlerTest { - return handlerTest{h} -} - -func (ht handlerTest) rawResponse(req string) string { - reqb := reqBytes(req) - var output bytes.Buffer - conn := &rwTestConn{ - Reader: bytes.NewReader(reqb), - Writer: &output, - closec: make(chan bool, 1), - } - ln := &oneConnListener{conn: conn} - go Serve(ln, ht.handler) - <-conn.closec - return output.String() -} - -func TestConsumingBodyOnNextConn(t *testing.T) { - conn := new(testConn) - for i := 0; i < 2; i++ { - conn.readBuf.Write([]byte( - "POST / HTTP/1.1\r\n" + - "Host: test\r\n" + - "Content-Length: 11\r\n" + - "\r\n" + - "foo=1&bar=1")) - } - - reqNum := 0 - ch := make(chan *Request) - servech := make(chan error) - listener := &oneConnListener{conn} - handler := func(res ResponseWriter, req *Request) { - reqNum++ - ch <- req - } - - go func() { - servech <- Serve(listener, HandlerFunc(handler)) - }() - - var req *Request - req = <-ch - if req == nil { - t.Fatal("Got nil first request.") - } - if req.Method != "POST" { - t.Errorf("For request #1's method, got %q; expected %q", - req.Method, "POST") - } - - req = <-ch - if req == nil { - t.Fatal("Got nil first request.") - } - if req.Method != "POST" { - t.Errorf("For request #2's method, got %q; expected %q", - req.Method, "POST") - } - - if serveerr := <-servech; serveerr != io.EOF { - t.Errorf("Serve returned %q; expected EOF", serveerr) - } -} - -type stringHandler string - -func (s stringHandler) ServeHTTP(w ResponseWriter, r *Request) { - w.Header().Set("Result", string(s)) -} - -var handlers = []struct { - pattern string - msg string -}{ - {"/", "Default"}, - {"/someDir/", "someDir"}, - {"someHost.com/someDir/", "someHost.com/someDir"}, -} - -var vtests = []struct { - url string - expected string -}{ - {"http://localhost/someDir/apage", "someDir"}, - {"http://localhost/otherDir/apage", "Default"}, - {"http://someHost.com/someDir/apage", "someHost.com/someDir"}, - {"http://otherHost.com/someDir/apage", "someDir"}, - {"http://otherHost.com/aDir/apage", "Default"}, - // redirections for trees - {"http://localhost/someDir", "/someDir/"}, - {"http://someHost.com/someDir", "/someDir/"}, -} - -func TestHostHandlers(t *testing.T) { - defer afterTest(t) - mux := NewServeMux() - for _, h := range handlers { - mux.Handle(h.pattern, stringHandler(h.msg)) - } - ts := httptest.NewServer(mux) - defer ts.Close() - - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - cc := httputil.NewClientConn(conn, nil) - for _, vt := range vtests { - var r *Response - var req Request - if req.URL, err = url.Parse(vt.url); err != nil { - t.Errorf("cannot parse url: %v", err) - continue - } - if err := cc.Write(&req); err != nil { - t.Errorf("writing request: %v", err) - continue - } - r, err := cc.Read(&req) - if err != nil { - t.Errorf("reading response: %v", err) - continue - } - switch r.StatusCode { - case StatusOK: - s := r.Header.Get("Result") - if s != vt.expected { - t.Errorf("Get(%q) = %q, want %q", vt.url, s, vt.expected) - } - case StatusMovedPermanently: - s := r.Header.Get("Location") - if s != vt.expected { - t.Errorf("Get(%q) = %q, want %q", vt.url, s, vt.expected) - } - default: - t.Errorf("Get(%q) unhandled status code %d", vt.url, r.StatusCode) - } - } -} - -var serveMuxRegister = []struct { - pattern string - h Handler -}{ - {"/dir/", serve(200)}, - {"/search", serve(201)}, - {"codesearch.google.com/search", serve(202)}, - {"codesearch.google.com/", serve(203)}, - {"example.com/", HandlerFunc(checkQueryStringHandler)}, -} - -// serve returns a handler that sends a response with the given code. -func serve(code int) HandlerFunc { - return func(w ResponseWriter, r *Request) { - w.WriteHeader(code) - } -} - -// checkQueryStringHandler checks if r.URL.RawQuery has the same value -// as the URL excluding the scheme and the query string and sends 200 -// response code if it is, 500 otherwise. -func checkQueryStringHandler(w ResponseWriter, r *Request) { - u := *r.URL - u.Scheme = "http" - u.Host = r.Host - u.RawQuery = "" - if "http://"+r.URL.RawQuery == u.String() { - w.WriteHeader(200) - } else { - w.WriteHeader(500) - } -} - -var serveMuxTests = []struct { - method string - host string - path string - code int - pattern string -}{ - {"GET", "google.com", "/", 404, ""}, - {"GET", "google.com", "/dir", 301, "/dir/"}, - {"GET", "google.com", "/dir/", 200, "/dir/"}, - {"GET", "google.com", "/dir/file", 200, "/dir/"}, - {"GET", "google.com", "/search", 201, "/search"}, - {"GET", "google.com", "/search/", 404, ""}, - {"GET", "google.com", "/search/foo", 404, ""}, - {"GET", "codesearch.google.com", "/search", 202, "codesearch.google.com/search"}, - {"GET", "codesearch.google.com", "/search/", 203, "codesearch.google.com/"}, - {"GET", "codesearch.google.com", "/search/foo", 203, "codesearch.google.com/"}, - {"GET", "codesearch.google.com", "/", 203, "codesearch.google.com/"}, - {"GET", "images.google.com", "/search", 201, "/search"}, - {"GET", "images.google.com", "/search/", 404, ""}, - {"GET", "images.google.com", "/search/foo", 404, ""}, - {"GET", "google.com", "/../search", 301, "/search"}, - {"GET", "google.com", "/dir/..", 301, ""}, - {"GET", "google.com", "/dir/..", 301, ""}, - {"GET", "google.com", "/dir/./file", 301, "/dir/"}, - - // The /foo -> /foo/ redirect applies to CONNECT requests - // but the path canonicalization does not. - {"CONNECT", "google.com", "/dir", 301, "/dir/"}, - {"CONNECT", "google.com", "/../search", 404, ""}, - {"CONNECT", "google.com", "/dir/..", 200, "/dir/"}, - {"CONNECT", "google.com", "/dir/..", 200, "/dir/"}, - {"CONNECT", "google.com", "/dir/./file", 200, "/dir/"}, -} - -func TestServeMuxHandler(t *testing.T) { - mux := NewServeMux() - for _, e := range serveMuxRegister { - mux.Handle(e.pattern, e.h) - } - - for _, tt := range serveMuxTests { - r := &Request{ - Method: tt.method, - Host: tt.host, - URL: &url.URL{ - Path: tt.path, - }, - } - h, pattern := mux.Handler(r) - rr := httptest.NewRecorder() - h.ServeHTTP(rr, r) - if pattern != tt.pattern || rr.Code != tt.code { - t.Errorf("%s %s %s = %d, %q, want %d, %q", tt.method, tt.host, tt.path, rr.Code, pattern, tt.code, tt.pattern) - } - } -} - -var serveMuxTests2 = []struct { - method string - host string - url string - code int - redirOk bool -}{ - {"GET", "google.com", "/", 404, false}, - {"GET", "example.com", "/test/?example.com/test/", 200, false}, - {"GET", "example.com", "test/?example.com/test/", 200, true}, -} - -// TestServeMuxHandlerRedirects tests that automatic redirects generated by -// mux.Handler() shouldn't clear the request's query string. -func TestServeMuxHandlerRedirects(t *testing.T) { - mux := NewServeMux() - for _, e := range serveMuxRegister { - mux.Handle(e.pattern, e.h) - } - - for _, tt := range serveMuxTests2 { - tries := 1 - turl := tt.url - for tries > 0 { - u, e := url.Parse(turl) - if e != nil { - t.Fatal(e) - } - r := &Request{ - Method: tt.method, - Host: tt.host, - URL: u, - } - h, _ := mux.Handler(r) - rr := httptest.NewRecorder() - h.ServeHTTP(rr, r) - if rr.Code != 301 { - if rr.Code != tt.code { - t.Errorf("%s %s %s = %d, want %d", tt.method, tt.host, tt.url, rr.Code, tt.code) - } - break - } - if !tt.redirOk { - t.Errorf("%s %s %s, unexpected redirect", tt.method, tt.host, tt.url) - break - } - turl = rr.HeaderMap.Get("Location") - tries-- - } - if tries < 0 { - t.Errorf("%s %s %s, too many redirects", tt.method, tt.host, tt.url) - } - } -} - -// Tests for http://code.google.com/p/go/issues/detail?id=900 -func TestMuxRedirectLeadingSlashes(t *testing.T) { - paths := []string{"//foo.txt", "///foo.txt", "/../../foo.txt"} - for _, path := range paths { - req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + path + " HTTP/1.1\r\nHost: test\r\n\r\n"))) - if err != nil { - t.Errorf("%s", err) - } - mux := NewServeMux() - resp := httptest.NewRecorder() - - mux.ServeHTTP(resp, req) - - if loc, expected := resp.Header().Get("Location"), "/foo.txt"; loc != expected { - t.Errorf("Expected Location header set to %q; got %q", expected, loc) - return - } - - if code, expected := resp.Code, StatusMovedPermanently; code != expected { - t.Errorf("Expected response code of StatusMovedPermanently; got %d", code) - return - } - } -} - -func TestServerTimeouts(t *testing.T) { - if runtime.GOOS == "plan9" { - t.Skip("skipping test; see http://golang.org/issue/7237") - } - defer afterTest(t) - reqNum := 0 - ts := httptest.NewUnstartedServer(HandlerFunc(func(res ResponseWriter, req *Request) { - reqNum++ - fmt.Fprintf(res, "req=%d", reqNum) - })) - ts.Config.ReadTimeout = 250 * time.Millisecond - ts.Config.WriteTimeout = 250 * time.Millisecond - ts.Start() - defer ts.Close() - - // Hit the HTTP server successfully. - tr := &Transport{DisableKeepAlives: true} // they interfere with this test - defer tr.CloseIdleConnections() - c := &Client{Transport: tr} - r, err := c.Get(ts.URL) - if err != nil { - t.Fatalf("http Get #1: %v", err) - } - got, _ := ioutil.ReadAll(r.Body) - expected := "req=1" - if string(got) != expected { - t.Errorf("Unexpected response for request #1; got %q; expected %q", - string(got), expected) - } - - // Slow client that should timeout. - t1 := time.Now() - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("Dial: %v", err) - } - buf := make([]byte, 1) - n, err := conn.Read(buf) - latency := time.Since(t1) - if n != 0 || err != io.EOF { - t.Errorf("Read = %v, %v, wanted %v, %v", n, err, 0, io.EOF) - } - if latency < 200*time.Millisecond /* fudge from 250 ms above */ { - t.Errorf("got EOF after %s, want >= %s", latency, 200*time.Millisecond) - } - - // Hit the HTTP server successfully again, verifying that the - // previous slow connection didn't run our handler. (that we - // get "req=2", not "req=3") - r, err = Get(ts.URL) - if err != nil { - t.Fatalf("http Get #2: %v", err) - } - got, _ = ioutil.ReadAll(r.Body) - expected = "req=2" - if string(got) != expected { - t.Errorf("Get #2 got %q, want %q", string(got), expected) - } - - if !testing.Short() { - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("Dial: %v", err) - } - defer conn.Close() - go io.Copy(ioutil.Discard, conn) - for i := 0; i < 5; i++ { - _, err := conn.Write([]byte("GET / HTTP/1.1\r\nHost: foo\r\n\r\n")) - if err != nil { - t.Fatalf("on write %d: %v", i, err) - } - time.Sleep(ts.Config.ReadTimeout / 2) - } - } -} - -// golang.org/issue/4741 -- setting only a write timeout that triggers -// shouldn't cause a handler to block forever on reads (next HTTP -// request) that will never happen. -func TestOnlyWriteTimeout(t *testing.T) { - if runtime.GOOS == "plan9" { - t.Skip("skipping test; see http://golang.org/issue/7237") - } - defer afterTest(t) - var conn net.Conn - var afterTimeoutErrc = make(chan error, 1) - ts := httptest.NewUnstartedServer(HandlerFunc(func(w ResponseWriter, req *Request) { - buf := make([]byte, 512<<10) - _, err := w.Write(buf) - if err != nil { - t.Errorf("handler Write error: %v", err) - return - } - conn.SetWriteDeadline(time.Now().Add(-30 * time.Second)) - _, err = w.Write(buf) - afterTimeoutErrc <- err - })) - ts.Listener = trackLastConnListener{ts.Listener, &conn} - ts.Start() - defer ts.Close() - - tr := &Transport{DisableKeepAlives: false} - defer tr.CloseIdleConnections() - c := &Client{Transport: tr} - - errc := make(chan error) - go func() { - res, err := c.Get(ts.URL) - if err != nil { - errc <- err - return - } - _, err = io.Copy(ioutil.Discard, res.Body) - errc <- err - }() - select { - case err := <-errc: - if err == nil { - t.Errorf("expected an error from Get request") - } - case <-time.After(5 * time.Second): - t.Fatal("timeout waiting for Get error") - } - if err := <-afterTimeoutErrc; err == nil { - t.Error("expected write error after timeout") - } -} - -// trackLastConnListener tracks the last net.Conn that was accepted. -type trackLastConnListener struct { - net.Listener - last *net.Conn // destination -} - -func (l trackLastConnListener) Accept() (c net.Conn, err error) { - c, err = l.Listener.Accept() - *l.last = c - return -} - -// TestIdentityResponse verifies that a handler can unset -func TestIdentityResponse(t *testing.T) { - defer afterTest(t) - handler := HandlerFunc(func(rw ResponseWriter, req *Request) { - rw.Header().Set("Content-Length", "3") - rw.Header().Set("Transfer-Encoding", req.FormValue("te")) - switch { - case req.FormValue("overwrite") == "1": - _, err := rw.Write([]byte("foo TOO LONG")) - if err != ErrContentLength { - t.Errorf("expected ErrContentLength; got %v", err) - } - case req.FormValue("underwrite") == "1": - rw.Header().Set("Content-Length", "500") - rw.Write([]byte("too short")) - default: - rw.Write([]byte("foo")) - } - }) - - ts := httptest.NewServer(handler) - defer ts.Close() - - // Note: this relies on the assumption (which is true) that - // Get sends HTTP/1.1 or greater requests. Otherwise the - // server wouldn't have the choice to send back chunked - // responses. - for _, te := range []string{"", "identity"} { - url := ts.URL + "/?te=" + te - res, err := Get(url) - if err != nil { - t.Fatalf("error with Get of %s: %v", url, err) - } - if cl, expected := res.ContentLength, int64(3); cl != expected { - t.Errorf("for %s expected res.ContentLength of %d; got %d", url, expected, cl) - } - if cl, expected := res.Header.Get("Content-Length"), "3"; cl != expected { - t.Errorf("for %s expected Content-Length header of %q; got %q", url, expected, cl) - } - if tl, expected := len(res.TransferEncoding), 0; tl != expected { - t.Errorf("for %s expected len(res.TransferEncoding) of %d; got %d (%v)", - url, expected, tl, res.TransferEncoding) - } - res.Body.Close() - } - - // Verify that ErrContentLength is returned - url := ts.URL + "/?overwrite=1" - res, err := Get(url) - if err != nil { - t.Fatalf("error with Get of %s: %v", url, err) - } - res.Body.Close() - - // Verify that the connection is closed when the declared Content-Length - // is larger than what the handler wrote. - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("error dialing: %v", err) - } - _, err = conn.Write([]byte("GET /?underwrite=1 HTTP/1.1\r\nHost: foo\r\n\r\n")) - if err != nil { - t.Fatalf("error writing: %v", err) - } - - // The ReadAll will hang for a failing test, so use a Timer to - // fail explicitly. - goTimeout(t, 2*time.Second, func() { - got, _ := ioutil.ReadAll(conn) - expectedSuffix := "\r\n\r\ntoo short" - if !strings.HasSuffix(string(got), expectedSuffix) { - t.Errorf("Expected output to end with %q; got response body %q", - expectedSuffix, string(got)) - } - }) -} - -func testTCPConnectionCloses(t *testing.T, req string, h Handler) { - defer afterTest(t) - s := httptest.NewServer(h) - defer s.Close() - - conn, err := net.Dial("tcp", s.Listener.Addr().String()) - if err != nil { - t.Fatal("dial error:", err) - } - defer conn.Close() - - _, err = fmt.Fprint(conn, req) - if err != nil { - t.Fatal("print error:", err) - } - - r := bufio.NewReader(conn) - res, err := ReadResponse(r, &Request{Method: "GET"}) - if err != nil { - t.Fatal("ReadResponse error:", err) - } - - didReadAll := make(chan bool, 1) - go func() { - select { - case <-time.After(5 * time.Second): - t.Error("body not closed after 5s") - return - case <-didReadAll: - } - }() - - _, err = ioutil.ReadAll(r) - if err != nil { - t.Fatal("read error:", err) - } - didReadAll <- true - - if !res.Close { - t.Errorf("Response.Close = false; want true") - } -} - -// TestServeHTTP10Close verifies that HTTP/1.0 requests won't be kept alive. -func TestServeHTTP10Close(t *testing.T) { - testTCPConnectionCloses(t, "GET / HTTP/1.0\r\n\r\n", HandlerFunc(func(w ResponseWriter, r *Request) { - ServeFile(w, r, "testdata/file") - })) -} - -// TestClientCanClose verifies that clients can also force a connection to close. -func TestClientCanClose(t *testing.T) { - testTCPConnectionCloses(t, "GET / HTTP/1.1\r\nConnection: close\r\n\r\n", HandlerFunc(func(w ResponseWriter, r *Request) { - // Nothing. - })) -} - -// TestHandlersCanSetConnectionClose verifies that handlers can force a connection to close, -// even for HTTP/1.1 requests. -func TestHandlersCanSetConnectionClose11(t *testing.T) { - testTCPConnectionCloses(t, "GET / HTTP/1.1\r\n\r\n", HandlerFunc(func(w ResponseWriter, r *Request) { - w.Header().Set("Connection", "close") - })) -} - -func TestHandlersCanSetConnectionClose10(t *testing.T) { - testTCPConnectionCloses(t, "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n", HandlerFunc(func(w ResponseWriter, r *Request) { - w.Header().Set("Connection", "close") - })) -} - -func TestSetsRemoteAddr(t *testing.T) { - defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - fmt.Fprintf(w, "%s", r.RemoteAddr) - })) - defer ts.Close() - - res, err := Get(ts.URL) - if err != nil { - t.Fatalf("Get error: %v", err) - } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatalf("ReadAll error: %v", err) - } - ip := string(body) - if !strings.HasPrefix(ip, "127.0.0.1:") && !strings.HasPrefix(ip, "[::1]:") { - t.Fatalf("Expected local addr; got %q", ip) - } -} - -func TestChunkedResponseHeaders(t *testing.T) { - defer afterTest(t) - log.SetOutput(ioutil.Discard) // is noisy otherwise - defer log.SetOutput(os.Stderr) - - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - w.Header().Set("Content-Length", "intentional gibberish") // we check that this is deleted - w.(Flusher).Flush() - fmt.Fprintf(w, "I am a chunked response.") - })) - defer ts.Close() - - res, err := Get(ts.URL) - if err != nil { - t.Fatalf("Get error: %v", err) - } - defer res.Body.Close() - if g, e := res.ContentLength, int64(-1); g != e { - t.Errorf("expected ContentLength of %d; got %d", e, g) - } - if g, e := res.TransferEncoding, []string{"chunked"}; !reflect.DeepEqual(g, e) { - t.Errorf("expected TransferEncoding of %v; got %v", e, g) - } - if _, haveCL := res.Header["Content-Length"]; haveCL { - t.Errorf("Unexpected Content-Length") - } -} - -// Test304Responses verifies that 304s don't declare that they're -// chunking in their response headers and aren't allowed to produce -// output. -func Test304Responses(t *testing.T) { - defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - w.WriteHeader(StatusNotModified) - _, err := w.Write([]byte("illegal body")) - if err != ErrBodyNotAllowed { - t.Errorf("on Write, expected ErrBodyNotAllowed, got %v", err) - } - })) - defer ts.Close() - res, err := Get(ts.URL) - if err != nil { - t.Error(err) - } - if len(res.TransferEncoding) > 0 { - t.Errorf("expected no TransferEncoding; got %v", res.TransferEncoding) - } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Error(err) - } - if len(body) > 0 { - t.Errorf("got unexpected body %q", string(body)) - } -} - -// TestHeadResponses verifies that all MIME type sniffing and Content-Length -// counting of GET requests also happens on HEAD requests. -func TestHeadResponses(t *testing.T) { - defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - _, err := w.Write([]byte("")) - if err != nil { - t.Errorf("ResponseWriter.Write: %v", err) - } - - // Also exercise the ReaderFrom path - _, err = io.Copy(w, strings.NewReader("789a")) - if err != nil { - t.Errorf("Copy(ResponseWriter, ...): %v", err) - } - })) - defer ts.Close() - res, err := Head(ts.URL) - if err != nil { - t.Error(err) - } - if len(res.TransferEncoding) > 0 { - t.Errorf("expected no TransferEncoding; got %v", res.TransferEncoding) - } - if ct := res.Header.Get("Content-Type"); ct != "text/html; charset=utf-8" { - t.Errorf("Content-Type: %q; want text/html; charset=utf-8", ct) - } - if v := res.ContentLength; v != 10 { - t.Errorf("Content-Length: %d; want 10", v) - } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Error(err) - } - if len(body) > 0 { - t.Errorf("got unexpected body %q", string(body)) - } -} - -func TestTLSHandshakeTimeout(t *testing.T) { - if runtime.GOOS == "plan9" { - t.Skip("skipping test; see http://golang.org/issue/7237") - } - defer afterTest(t) - ts := httptest.NewUnstartedServer(HandlerFunc(func(w ResponseWriter, r *Request) {})) - errc := make(chanWriter, 10) // but only expecting 1 - ts.Config.ReadTimeout = 250 * time.Millisecond - ts.Config.ErrorLog = log.New(errc, "", 0) - ts.StartTLS() - defer ts.Close() - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("Dial: %v", err) - } - defer conn.Close() - goTimeout(t, 10*time.Second, func() { - var buf [1]byte - n, err := conn.Read(buf[:]) - if err == nil || n != 0 { - t.Errorf("Read = %d, %v; want an error and no bytes", n, err) - } - }) - select { - case v := <-errc: - if !strings.Contains(v, "timeout") && !strings.Contains(v, "TLS handshake") { - t.Errorf("expected a TLS handshake timeout error; got %q", v) - } - case <-time.After(5 * time.Second): - t.Errorf("timeout waiting for logged error") - } -} - -func TestTLSServer(t *testing.T) { - defer afterTest(t) - ts := httptest.NewTLSServer(HandlerFunc(func(w ResponseWriter, r *Request) { - if r.TLS != nil { - w.Header().Set("X-TLS-Set", "true") - if r.TLS.HandshakeComplete { - w.Header().Set("X-TLS-HandshakeComplete", "true") - } - } - })) - ts.Config.ErrorLog = log.New(ioutil.Discard, "", 0) - defer ts.Close() - - // Connect an idle TCP connection to this server before we run - // our real tests. This idle connection used to block forever - // in the TLS handshake, preventing future connections from - // being accepted. It may prevent future accidental blocking - // in newConn. - idleConn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("Dial: %v", err) - } - defer idleConn.Close() - goTimeout(t, 10*time.Second, func() { - if !strings.HasPrefix(ts.URL, "https://") { - t.Errorf("expected test TLS server to start with https://, got %q", ts.URL) - return - } - noVerifyTransport := &Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - } - client := &Client{Transport: noVerifyTransport} - res, err := client.Get(ts.URL) - if err != nil { - t.Error(err) - return - } - if res == nil { - t.Errorf("got nil Response") - return - } - defer res.Body.Close() - if res.Header.Get("X-TLS-Set") != "true" { - t.Errorf("expected X-TLS-Set response header") - return - } - if res.Header.Get("X-TLS-HandshakeComplete") != "true" { - t.Errorf("expected X-TLS-HandshakeComplete header") - } - }) -} - -type serverExpectTest struct { - contentLength int // of request body - chunked bool - expectation string // e.g. "100-continue" - readBody bool // whether handler should read the body (if false, sends StatusUnauthorized) - expectedResponse string // expected substring in first line of http response -} - -func expectTest(contentLength int, expectation string, readBody bool, expectedResponse string) serverExpectTest { - return serverExpectTest{ - contentLength: contentLength, - expectation: expectation, - readBody: readBody, - expectedResponse: expectedResponse, - } -} - -var serverExpectTests = []serverExpectTest{ - // Normal 100-continues, case-insensitive. - expectTest(100, "100-continue", true, "100 Continue"), - expectTest(100, "100-cOntInUE", true, "100 Continue"), - - // No 100-continue. - expectTest(100, "", true, "200 OK"), - - // 100-continue but requesting client to deny us, - // so it never reads the body. - expectTest(100, "100-continue", false, "401 Unauthorized"), - // Likewise without 100-continue: - expectTest(100, "", false, "401 Unauthorized"), - - // Non-standard expectations are failures - expectTest(0, "a-pony", false, "417 Expectation Failed"), - - // Expect-100 requested but no body (is apparently okay: Issue 7625) - expectTest(0, "100-continue", true, "200 OK"), - // Expect-100 requested but handler doesn't read the body - expectTest(0, "100-continue", false, "401 Unauthorized"), - // Expect-100 continue with no body, but a chunked body. - { - expectation: "100-continue", - readBody: true, - chunked: true, - expectedResponse: "100 Continue", - }, -} - -// Tests that the server responds to the "Expect" request header -// correctly. -func TestServerExpect(t *testing.T) { - defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - // Note using r.FormValue("readbody") because for POST - // requests that would read from r.Body, which we only - // conditionally want to do. - if strings.Contains(r.URL.RawQuery, "readbody=true") { - ioutil.ReadAll(r.Body) - w.Write([]byte("Hi")) - } else { - w.WriteHeader(StatusUnauthorized) - } - })) - defer ts.Close() - - runTest := func(test serverExpectTest) { - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("Dial: %v", err) - } - defer conn.Close() - - // Only send the body immediately if we're acting like an HTTP client - // that doesn't send 100-continue expectations. - writeBody := test.contentLength != 0 && strings.ToLower(test.expectation) != "100-continue" - - go func() { - contentLen := fmt.Sprintf("Content-Length: %d", test.contentLength) - if test.chunked { - contentLen = "Transfer-Encoding: chunked" - } - _, err := fmt.Fprintf(conn, "POST /?readbody=%v HTTP/1.1\r\n"+ - "Connection: close\r\n"+ - "%s\r\n"+ - "Expect: %s\r\nHost: foo\r\n\r\n", - test.readBody, contentLen, test.expectation) - if err != nil { - t.Errorf("On test %#v, error writing request headers: %v", test, err) - return - } - if writeBody { - var targ io.WriteCloser = struct { - io.Writer - io.Closer - }{ - conn, - ioutil.NopCloser(nil), - } - if test.chunked { - targ = httputil.NewChunkedWriter(conn) - } - body := strings.Repeat("A", test.contentLength) - _, err = fmt.Fprint(targ, body) - if err == nil { - err = targ.Close() - } - if err != nil { - if !test.readBody { - // Server likely already hung up on us. - // See larger comment below. - t.Logf("On test %#v, acceptable error writing request body: %v", test, err) - return - } - t.Errorf("On test %#v, error writing request body: %v", test, err) - } - } - }() - bufr := bufio.NewReader(conn) - line, err := bufr.ReadString('\n') - if err != nil { - if writeBody && !test.readBody { - // This is an acceptable failure due to a possible TCP race: - // We were still writing data and the server hung up on us. A TCP - // implementation may send a RST if our request body data was known - // to be lost, which may trigger our reads to fail. - // See RFC 1122 page 88. - t.Logf("On test %#v, acceptable error from ReadString: %v", test, err) - return - } - t.Fatalf("On test %#v, ReadString: %v", test, err) - } - if !strings.Contains(line, test.expectedResponse) { - t.Errorf("On test %#v, got first line = %q; want %q", test, line, test.expectedResponse) - } - } - - for _, test := range serverExpectTests { - runTest(test) - } -} - -// Under a ~256KB (maxPostHandlerReadBytes) threshold, the server -// should consume client request bodies that a handler didn't read. -func TestServerUnreadRequestBodyLittle(t *testing.T) { - conn := new(testConn) - body := strings.Repeat("x", 100<<10) - conn.readBuf.Write([]byte(fmt.Sprintf( - "POST / HTTP/1.1\r\n"+ - "Host: test\r\n"+ - "Content-Length: %d\r\n"+ - "\r\n", len(body)))) - conn.readBuf.Write([]byte(body)) - - done := make(chan bool) - - ls := &oneConnListener{conn} - go Serve(ls, HandlerFunc(func(rw ResponseWriter, req *Request) { - defer close(done) - if conn.readBuf.Len() < len(body)/2 { - t.Errorf("on request, read buffer length is %d; expected about 100 KB", conn.readBuf.Len()) - } - rw.WriteHeader(200) - rw.(Flusher).Flush() - if g, e := conn.readBuf.Len(), 0; g != e { - t.Errorf("after WriteHeader, read buffer length is %d; want %d", g, e) - } - if c := rw.Header().Get("Connection"); c != "" { - t.Errorf(`Connection header = %q; want ""`, c) - } - })) - <-done -} - -// Over a ~256KB (maxPostHandlerReadBytes) threshold, the server -// should ignore client request bodies that a handler didn't read -// and close the connection. -func TestServerUnreadRequestBodyLarge(t *testing.T) { - conn := new(testConn) - body := strings.Repeat("x", 1<<20) - conn.readBuf.Write([]byte(fmt.Sprintf( - "POST / HTTP/1.1\r\n"+ - "Host: test\r\n"+ - "Content-Length: %d\r\n"+ - "\r\n", len(body)))) - conn.readBuf.Write([]byte(body)) - conn.closec = make(chan bool, 1) - - ls := &oneConnListener{conn} - go Serve(ls, HandlerFunc(func(rw ResponseWriter, req *Request) { - if conn.readBuf.Len() < len(body)/2 { - t.Errorf("on request, read buffer length is %d; expected about 1MB", conn.readBuf.Len()) - } - rw.WriteHeader(200) - rw.(Flusher).Flush() - if conn.readBuf.Len() < len(body)/2 { - t.Errorf("post-WriteHeader, read buffer length is %d; expected about 1MB", conn.readBuf.Len()) - } - })) - <-conn.closec - - if res := conn.writeBuf.String(); !strings.Contains(res, "Connection: close") { - t.Errorf("Expected a Connection: close header; got response: %s", res) - } -} - -func TestTimeoutHandler(t *testing.T) { - defer afterTest(t) - sendHi := make(chan bool, 1) - writeErrors := make(chan error, 1) - sayHi := HandlerFunc(func(w ResponseWriter, r *Request) { - <-sendHi - _, werr := w.Write([]byte("hi")) - writeErrors <- werr - }) - timeout := make(chan time.Time, 1) // write to this to force timeouts - ts := httptest.NewServer(NewTestTimeoutHandler(sayHi, timeout)) - defer ts.Close() - - // Succeed without timing out: - sendHi <- true - res, err := Get(ts.URL) - if err != nil { - t.Error(err) - } - if g, e := res.StatusCode, StatusOK; g != e { - t.Errorf("got res.StatusCode %d; expected %d", g, e) - } - body, _ := ioutil.ReadAll(res.Body) - if g, e := string(body), "hi"; g != e { - t.Errorf("got body %q; expected %q", g, e) - } - if g := <-writeErrors; g != nil { - t.Errorf("got unexpected Write error on first request: %v", g) - } - - // Times out: - timeout <- time.Time{} - res, err = Get(ts.URL) - if err != nil { - t.Error(err) - } - if g, e := res.StatusCode, StatusServiceUnavailable; g != e { - t.Errorf("got res.StatusCode %d; expected %d", g, e) - } - body, _ = ioutil.ReadAll(res.Body) - if !strings.Contains(string(body), "Timeout") { - t.Errorf("expected timeout body; got %q", string(body)) - } - - // Now make the previously-timed out handler speak again, - // which verifies the panic is handled: - sendHi <- true - if g, e := <-writeErrors, ErrHandlerTimeout; g != e { - t.Errorf("expected Write error of %v; got %v", e, g) - } -} - -// Verifies we don't path.Clean() on the wrong parts in redirects. -func TestRedirectMunging(t *testing.T) { - req, _ := NewRequest("GET", "http://example.com/", nil) - - resp := httptest.NewRecorder() - Redirect(resp, req, "/foo?next=http://bar.com/", 302) - if g, e := resp.Header().Get("Location"), "/foo?next=http://bar.com/"; g != e { - t.Errorf("Location header was %q; want %q", g, e) - } - - resp = httptest.NewRecorder() - Redirect(resp, req, "http://localhost:8080/_ah/login?continue=http://localhost:8080/", 302) - if g, e := resp.Header().Get("Location"), "http://localhost:8080/_ah/login?continue=http://localhost:8080/"; g != e { - t.Errorf("Location header was %q; want %q", g, e) - } -} - -func TestRedirectBadPath(t *testing.T) { - // This used to crash. It's not valid input (bad path), but it - // shouldn't crash. - rr := httptest.NewRecorder() - req := &Request{ - Method: "GET", - URL: &url.URL{ - Scheme: "http", - Path: "not-empty-but-no-leading-slash", // bogus - }, - } - Redirect(rr, req, "", 304) - if rr.Code != 304 { - t.Errorf("Code = %d; want 304", rr.Code) - } -} - -// TestZeroLengthPostAndResponse exercises an optimization done by the Transport: -// when there is no body (either because the method doesn't permit a body, or an -// explicit Content-Length of zero is present), then the transport can re-use the -// connection immediately. But when it re-uses the connection, it typically closes -// the previous request's body, which is not optimal for zero-lengthed bodies, -// as the client would then see http.ErrBodyReadAfterClose and not 0, io.EOF. -func TestZeroLengthPostAndResponse(t *testing.T) { - defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) { - all, err := ioutil.ReadAll(r.Body) - if err != nil { - t.Fatalf("handler ReadAll: %v", err) - } - if len(all) != 0 { - t.Errorf("handler got %d bytes; expected 0", len(all)) - } - rw.Header().Set("Content-Length", "0") - })) - defer ts.Close() - - req, err := NewRequest("POST", ts.URL, strings.NewReader("")) - if err != nil { - t.Fatal(err) - } - req.ContentLength = 0 - - var resp [5]*Response - for i := range resp { - resp[i], err = DefaultClient.Do(req) - if err != nil { - t.Fatalf("client post #%d: %v", i, err) - } - } - - for i := range resp { - all, err := ioutil.ReadAll(resp[i].Body) - if err != nil { - t.Fatalf("req #%d: client ReadAll: %v", i, err) - } - if len(all) != 0 { - t.Errorf("req #%d: client got %d bytes; expected 0", i, len(all)) - } - } -} - -func TestHandlerPanicNil(t *testing.T) { - testHandlerPanic(t, false, nil) -} - -func TestHandlerPanic(t *testing.T) { - testHandlerPanic(t, false, "intentional death for testing") -} - -func TestHandlerPanicWithHijack(t *testing.T) { - testHandlerPanic(t, true, "intentional death for testing") -} - -func testHandlerPanic(t *testing.T, withHijack bool, panicValue interface{}) { - defer afterTest(t) - // Unlike the other tests that set the log output to ioutil.Discard - // to quiet the output, this test uses a pipe. The pipe serves three - // purposes: - // - // 1) The log.Print from the http server (generated by the caught - // panic) will go to the pipe instead of stderr, making the - // output quiet. - // - // 2) We read from the pipe to verify that the handler - // actually caught the panic and logged something. - // - // 3) The blocking Read call prevents this TestHandlerPanic - // function from exiting before the HTTP server handler - // finishes crashing. If this text function exited too - // early (and its defer log.SetOutput(os.Stderr) ran), - // then the crash output could spill into the next test. - pr, pw := io.Pipe() - log.SetOutput(pw) - defer log.SetOutput(os.Stderr) - defer pw.Close() - - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - if withHijack { - rwc, _, err := w.(Hijacker).Hijack() - if err != nil { - t.Logf("unexpected error: %v", err) - } - defer rwc.Close() - } - panic(panicValue) - })) - defer ts.Close() - - // Do a blocking read on the log output pipe so its logging - // doesn't bleed into the next test. But wait only 5 seconds - // for it. - done := make(chan bool, 1) - go func() { - buf := make([]byte, 4<<10) - _, err := pr.Read(buf) - pr.Close() - if err != nil && err != io.EOF { - t.Error(err) - } - done <- true - }() - - _, err := Get(ts.URL) - if err == nil { - t.Logf("expected an error") - } - - if panicValue == nil { - return - } - - select { - case <-done: - return - case <-time.After(5 * time.Second): - t.Fatal("expected server handler to log an error") - } -} - -func TestNoDate(t *testing.T) { - defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - w.Header()["Date"] = nil - })) - defer ts.Close() - res, err := Get(ts.URL) - if err != nil { - t.Fatal(err) - } - _, present := res.Header["Date"] - if present { - t.Fatalf("Expected no Date header; got %v", res.Header["Date"]) - } -} - -func TestStripPrefix(t *testing.T) { - defer afterTest(t) - h := HandlerFunc(func(w ResponseWriter, r *Request) { - w.Header().Set("X-Path", r.URL.Path) - }) - ts := httptest.NewServer(StripPrefix("/foo", h)) - defer ts.Close() - - res, err := Get(ts.URL + "/foo/bar") - if err != nil { - t.Fatal(err) - } - if g, e := res.Header.Get("X-Path"), "/bar"; g != e { - t.Errorf("test 1: got %s, want %s", g, e) - } - res.Body.Close() - - res, err = Get(ts.URL + "/bar") - if err != nil { - t.Fatal(err) - } - if g, e := res.StatusCode, 404; g != e { - t.Errorf("test 2: got status %v, want %v", g, e) - } - res.Body.Close() -} - -func TestRequestLimit(t *testing.T) { - defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - t.Fatalf("didn't expect to get request in Handler") - })) - defer ts.Close() - req, _ := NewRequest("GET", ts.URL, nil) - var bytesPerHeader = len("header12345: val12345\r\n") - for i := 0; i < ((DefaultMaxHeaderBytes+4096)/bytesPerHeader)+1; i++ { - req.Header.Set(fmt.Sprintf("header%05d", i), fmt.Sprintf("val%05d", i)) - } - res, err := DefaultClient.Do(req) - if err != nil { - // Some HTTP clients may fail on this undefined behavior (server replying and - // closing the connection while the request is still being written), but - // we do support it (at least currently), so we expect a response below. - t.Fatalf("Do: %v", err) - } - defer res.Body.Close() - if res.StatusCode != 413 { - t.Fatalf("expected 413 response status; got: %d %s", res.StatusCode, res.Status) - } -} - type neverEnding byte func (b neverEnding) Read(p []byte) (n int, err error) { @@ -1420,1392 +56,3 @@ } return len(p), nil } - -type countReader struct { - r io.Reader - n *int64 -} - -func (cr countReader) Read(p []byte) (n int, err error) { - n, err = cr.r.Read(p) - atomic.AddInt64(cr.n, int64(n)) - return -} - -func TestRequestBodyLimit(t *testing.T) { - defer afterTest(t) - const limit = 1 << 20 - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - r.Body = MaxBytesReader(w, r.Body, limit) - n, err := io.Copy(ioutil.Discard, r.Body) - if err == nil { - t.Errorf("expected error from io.Copy") - } - if n != limit { - t.Errorf("io.Copy = %d, want %d", n, limit) - } - })) - defer ts.Close() - - nWritten := new(int64) - req, _ := NewRequest("POST", ts.URL, io.LimitReader(countReader{neverEnding('a'), nWritten}, limit*200)) - - // Send the POST, but don't care it succeeds or not. The - // remote side is going to reply and then close the TCP - // connection, and HTTP doesn't really define if that's - // allowed or not. Some HTTP clients will get the response - // and some (like ours, currently) will complain that the - // request write failed, without reading the response. - // - // But that's okay, since what we're really testing is that - // the remote side hung up on us before we wrote too much. - _, _ = DefaultClient.Do(req) - - if atomic.LoadInt64(nWritten) > limit*100 { - t.Errorf("handler restricted the request body to %d bytes, but client managed to write %d", - limit, nWritten) - } -} - -// TestClientWriteShutdown tests that if the client shuts down the write -// side of their TCP connection, the server doesn't send a 400 Bad Request. -func TestClientWriteShutdown(t *testing.T) { - if runtime.GOOS == "plan9" { - t.Skip("skipping test; see http://golang.org/issue/7237") - } - defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {})) - defer ts.Close() - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("Dial: %v", err) - } - err = conn.(*net.TCPConn).CloseWrite() - if err != nil { - t.Fatalf("Dial: %v", err) - } - donec := make(chan bool) - go func() { - defer close(donec) - bs, err := ioutil.ReadAll(conn) - if err != nil { - t.Fatalf("ReadAll: %v", err) - } - got := string(bs) - if got != "" { - t.Errorf("read %q from server; want nothing", got) - } - }() - select { - case <-donec: - case <-time.After(10 * time.Second): - t.Fatalf("timeout") - } -} - -// Tests that chunked server responses that write 1 byte at a time are -// buffered before chunk headers are added, not after chunk headers. -func TestServerBufferedChunking(t *testing.T) { - conn := new(testConn) - conn.readBuf.Write([]byte("GET / HTTP/1.1\r\n\r\n")) - conn.closec = make(chan bool, 1) - ls := &oneConnListener{conn} - go Serve(ls, HandlerFunc(func(rw ResponseWriter, req *Request) { - rw.(Flusher).Flush() // force the Header to be sent, in chunking mode, not counting the length - rw.Write([]byte{'x'}) - rw.Write([]byte{'y'}) - rw.Write([]byte{'z'}) - })) - <-conn.closec - if !bytes.HasSuffix(conn.writeBuf.Bytes(), []byte("\r\n\r\n3\r\nxyz\r\n0\r\n\r\n")) { - t.Errorf("response didn't end with a single 3 byte 'xyz' chunk; got:\n%q", - conn.writeBuf.Bytes()) - } -} - -// Tests that the server flushes its response headers out when it's -// ignoring the response body and waits a bit before forcefully -// closing the TCP connection, causing the client to get a RST. -// See http://golang.org/issue/3595 -func TestServerGracefulClose(t *testing.T) { - defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - Error(w, "bye", StatusUnauthorized) - })) - defer ts.Close() - - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - const bodySize = 5 << 20 - req := []byte(fmt.Sprintf("POST / HTTP/1.1\r\nHost: foo.com\r\nContent-Length: %d\r\n\r\n", bodySize)) - for i := 0; i < bodySize; i++ { - req = append(req, 'x') - } - writeErr := make(chan error) - go func() { - _, err := conn.Write(req) - writeErr <- err - }() - br := bufio.NewReader(conn) - lineNum := 0 - for { - line, err := br.ReadString('\n') - if err == io.EOF { - break - } - if err != nil { - t.Fatalf("ReadLine: %v", err) - } - lineNum++ - if lineNum == 1 && !strings.Contains(line, "401 Unauthorized") { - t.Errorf("Response line = %q; want a 401", line) - } - } - // Wait for write to finish. This is a broken pipe on both - // Darwin and Linux, but checking this isn't the point of - // the test. - <-writeErr -} - -func TestCaseSensitiveMethod(t *testing.T) { - defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - if r.Method != "get" { - t.Errorf(`Got method %q; want "get"`, r.Method) - } - })) - defer ts.Close() - req, _ := NewRequest("get", ts.URL, nil) - res, err := DefaultClient.Do(req) - if err != nil { - t.Error(err) - return - } - res.Body.Close() -} - -// TestContentLengthZero tests that for both an HTTP/1.0 and HTTP/1.1 -// request (both keep-alive), when a Handler never writes any -// response, the net/http package adds a "Content-Length: 0" response -// header. -func TestContentLengthZero(t *testing.T) { - ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) {})) - defer ts.Close() - - for _, version := range []string{"HTTP/1.0", "HTTP/1.1"} { - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("error dialing: %v", err) - } - _, err = fmt.Fprintf(conn, "GET / %v\r\nConnection: keep-alive\r\nHost: foo\r\n\r\n", version) - if err != nil { - t.Fatalf("error writing: %v", err) - } - req, _ := NewRequest("GET", "/", nil) - res, err := ReadResponse(bufio.NewReader(conn), req) - if err != nil { - t.Fatalf("error reading response: %v", err) - } - if te := res.TransferEncoding; len(te) > 0 { - t.Errorf("For version %q, Transfer-Encoding = %q; want none", version, te) - } - if cl := res.ContentLength; cl != 0 { - t.Errorf("For version %q, Content-Length = %v; want 0", version, cl) - } - conn.Close() - } -} - -func TestCloseNotifier(t *testing.T) { - defer afterTest(t) - gotReq := make(chan bool, 1) - sawClose := make(chan bool, 1) - ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) { - gotReq <- true - cc := rw.(CloseNotifier).CloseNotify() - <-cc - sawClose <- true - })) - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("error dialing: %v", err) - } - diec := make(chan bool) - go func() { - _, err = fmt.Fprintf(conn, "GET / HTTP/1.1\r\nConnection: keep-alive\r\nHost: foo\r\n\r\n") - if err != nil { - t.Fatal(err) - } - <-diec - conn.Close() - }() -For: - for { - select { - case <-gotReq: - diec <- true - case <-sawClose: - break For - case <-time.After(5 * time.Second): - t.Fatal("timeout") - } - } - ts.Close() -} - -func TestCloseNotifierChanLeak(t *testing.T) { - defer afterTest(t) - req := reqBytes("GET / HTTP/1.0\nHost: golang.org") - for i := 0; i < 20; i++ { - var output bytes.Buffer - conn := &rwTestConn{ - Reader: bytes.NewReader(req), - Writer: &output, - closec: make(chan bool, 1), - } - ln := &oneConnListener{conn: conn} - handler := HandlerFunc(func(rw ResponseWriter, r *Request) { - // Ignore the return value and never read from - // it, testing that we don't leak goroutines - // on the sending side: - _ = rw.(CloseNotifier).CloseNotify() - }) - go Serve(ln, handler) - <-conn.closec - } -} - -func TestOptions(t *testing.T) { - uric := make(chan string, 2) // only expect 1, but leave space for 2 - mux := NewServeMux() - mux.HandleFunc("/", func(w ResponseWriter, r *Request) { - uric <- r.RequestURI - }) - ts := httptest.NewServer(mux) - defer ts.Close() - - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - // An OPTIONS * request should succeed. - _, err = conn.Write([]byte("OPTIONS * HTTP/1.1\r\nHost: foo.com\r\n\r\n")) - if err != nil { - t.Fatal(err) - } - br := bufio.NewReader(conn) - res, err := ReadResponse(br, &Request{Method: "OPTIONS"}) - if err != nil { - t.Fatal(err) - } - if res.StatusCode != 200 { - t.Errorf("Got non-200 response to OPTIONS *: %#v", res) - } - - // A GET * request on a ServeMux should fail. - _, err = conn.Write([]byte("GET * HTTP/1.1\r\nHost: foo.com\r\n\r\n")) - if err != nil { - t.Fatal(err) - } - res, err = ReadResponse(br, &Request{Method: "GET"}) - if err != nil { - t.Fatal(err) - } - if res.StatusCode != 400 { - t.Errorf("Got non-400 response to GET *: %#v", res) - } - - res, err = Get(ts.URL + "/second") - if err != nil { - t.Fatal(err) - } - res.Body.Close() - if got := <-uric; got != "/second" { - t.Errorf("Handler saw request for %q; want /second", got) - } -} - -// Tests regarding the ordering of Write, WriteHeader, Header, and -// Flush calls. In Go 1.0, rw.WriteHeader immediately flushed the -// (*response).header to the wire. In Go 1.1, the actual wire flush is -// delayed, so we could maybe tack on a Content-Length and better -// Content-Type after we see more (or all) of the output. To preserve -// compatibility with Go 1, we need to be careful to track which -// headers were live at the time of WriteHeader, so we write the same -// ones, even if the handler modifies them (~erroneously) after the -// first Write. -func TestHeaderToWire(t *testing.T) { - tests := []struct { - name string - handler func(ResponseWriter, *Request) - check func(output string) error - }{ - { - name: "write without Header", - handler: func(rw ResponseWriter, r *Request) { - rw.Write([]byte("hello world")) - }, - check: func(got string) error { - if !strings.Contains(got, "Content-Length:") { - return errors.New("no content-length") - } - if !strings.Contains(got, "Content-Type: text/plain") { - return errors.New("no content-length") - } - return nil - }, - }, - { - name: "Header mutation before write", - handler: func(rw ResponseWriter, r *Request) { - h := rw.Header() - h.Set("Content-Type", "some/type") - rw.Write([]byte("hello world")) - h.Set("Too-Late", "bogus") - }, - check: func(got string) error { - if !strings.Contains(got, "Content-Length:") { - return errors.New("no content-length") - } - if !strings.Contains(got, "Content-Type: some/type") { - return errors.New("wrong content-type") - } - if strings.Contains(got, "Too-Late") { - return errors.New("don't want too-late header") - } - return nil - }, - }, - { - name: "write then useless Header mutation", - handler: func(rw ResponseWriter, r *Request) { - rw.Write([]byte("hello world")) - rw.Header().Set("Too-Late", "Write already wrote headers") - }, - check: func(got string) error { - if strings.Contains(got, "Too-Late") { - return errors.New("header appeared from after WriteHeader") - } - return nil - }, - }, - { - name: "flush then write", - handler: func(rw ResponseWriter, r *Request) { - rw.(Flusher).Flush() - rw.Write([]byte("post-flush")) - rw.Header().Set("Too-Late", "Write already wrote headers") - }, - check: func(got string) error { - if !strings.Contains(got, "Transfer-Encoding: chunked") { - return errors.New("not chunked") - } - if strings.Contains(got, "Too-Late") { - return errors.New("header appeared from after WriteHeader") - } - return nil - }, - }, - { - name: "header then flush", - handler: func(rw ResponseWriter, r *Request) { - rw.Header().Set("Content-Type", "some/type") - rw.(Flusher).Flush() - rw.Write([]byte("post-flush")) - rw.Header().Set("Too-Late", "Write already wrote headers") - }, - check: func(got string) error { - if !strings.Contains(got, "Transfer-Encoding: chunked") { - return errors.New("not chunked") - } - if strings.Contains(got, "Too-Late") { - return errors.New("header appeared from after WriteHeader") - } - if !strings.Contains(got, "Content-Type: some/type") { - return errors.New("wrong content-length") - } - return nil - }, - }, - { - name: "sniff-on-first-write content-type", - handler: func(rw ResponseWriter, r *Request) { - rw.Write([]byte("some html")) - rw.Header().Set("Content-Type", "x/wrong") - }, - check: func(got string) error { - if !strings.Contains(got, "Content-Type: text/html") { - return errors.New("wrong content-length; want html") - } - return nil - }, - }, - { - name: "explicit content-type wins", - handler: func(rw ResponseWriter, r *Request) { - rw.Header().Set("Content-Type", "some/type") - rw.Write([]byte("some html")) - }, - check: func(got string) error { - if !strings.Contains(got, "Content-Type: some/type") { - return errors.New("wrong content-length; want html") - } - return nil - }, - }, - { - name: "empty handler", - handler: func(rw ResponseWriter, r *Request) { - }, - check: func(got string) error { - if !strings.Contains(got, "Content-Type: text/plain") { - return errors.New("wrong content-length; want text/plain") - } - if !strings.Contains(got, "Content-Length: 0") { - return errors.New("want 0 content-length") - } - return nil - }, - }, - { - name: "only Header, no write", - handler: func(rw ResponseWriter, r *Request) { - rw.Header().Set("Some-Header", "some-value") - }, - check: func(got string) error { - if !strings.Contains(got, "Some-Header") { - return errors.New("didn't get header") - } - return nil - }, - }, - { - name: "WriteHeader call", - handler: func(rw ResponseWriter, r *Request) { - rw.WriteHeader(404) - rw.Header().Set("Too-Late", "some-value") - }, - check: func(got string) error { - if !strings.Contains(got, "404") { - return errors.New("wrong status") - } - if strings.Contains(got, "Some-Header") { - return errors.New("shouldn't have seen Too-Late") - } - return nil - }, - }, - } - for _, tc := range tests { - ht := newHandlerTest(HandlerFunc(tc.handler)) - got := ht.rawResponse("GET / HTTP/1.1\nHost: golang.org") - if err := tc.check(got); err != nil { - t.Errorf("%s: %v\nGot response:\n%s", tc.name, err, got) - } - } -} - -// goTimeout runs f, failing t if f takes more than ns to complete. -func goTimeout(t *testing.T, d time.Duration, f func()) { - ch := make(chan bool, 2) - timer := time.AfterFunc(d, func() { - t.Errorf("Timeout expired after %v", d) - ch <- true - }) - defer timer.Stop() - go func() { - defer func() { ch <- true }() - f() - }() - <-ch -} - -type errorListener struct { - errs []error -} - -func (l *errorListener) Accept() (c net.Conn, err error) { - if len(l.errs) == 0 { - return nil, io.EOF - } - err = l.errs[0] - l.errs = l.errs[1:] - return -} - -func (l *errorListener) Close() error { - return nil -} - -func (l *errorListener) Addr() net.Addr { - return dummyAddr("test-address") -} - -func TestAcceptMaxFds(t *testing.T) { - log.SetOutput(ioutil.Discard) // is noisy otherwise - defer log.SetOutput(os.Stderr) - - ln := &errorListener{[]error{ - &net.OpError{ - Op: "accept", - Err: syscall.EMFILE, - }}} - err := Serve(ln, HandlerFunc(HandlerFunc(func(ResponseWriter, *Request) {}))) - if err != io.EOF { - t.Errorf("got error %v, want EOF", err) - } -} - -func TestWriteAfterHijack(t *testing.T) { - req := reqBytes("GET / HTTP/1.1\nHost: golang.org") - var buf bytes.Buffer - wrotec := make(chan bool, 1) - conn := &rwTestConn{ - Reader: bytes.NewReader(req), - Writer: &buf, - closec: make(chan bool, 1), - } - handler := HandlerFunc(func(rw ResponseWriter, r *Request) { - conn, bufrw, err := rw.(Hijacker).Hijack() - if err != nil { - t.Error(err) - return - } - go func() { - bufrw.Write([]byte("[hijack-to-bufw]")) - bufrw.Flush() - conn.Write([]byte("[hijack-to-conn]")) - conn.Close() - wrotec <- true - }() - }) - ln := &oneConnListener{conn: conn} - go Serve(ln, handler) - <-conn.closec - <-wrotec - if g, w := buf.String(), "[hijack-to-bufw][hijack-to-conn]"; g != w { - t.Errorf("wrote %q; want %q", g, w) - } -} - -func TestDoubleHijack(t *testing.T) { - req := reqBytes("GET / HTTP/1.1\nHost: golang.org") - var buf bytes.Buffer - conn := &rwTestConn{ - Reader: bytes.NewReader(req), - Writer: &buf, - closec: make(chan bool, 1), - } - handler := HandlerFunc(func(rw ResponseWriter, r *Request) { - conn, _, err := rw.(Hijacker).Hijack() - if err != nil { - t.Error(err) - return - } - _, _, err = rw.(Hijacker).Hijack() - if err == nil { - t.Errorf("got err = nil; want err != nil") - } - conn.Close() - }) - ln := &oneConnListener{conn: conn} - go Serve(ln, handler) - <-conn.closec -} - -// http://code.google.com/p/go/issues/detail?id=5955 -// Note that this does not test the "request too large" -// exit path from the http server. This is intentional; -// not sending Connection: close is just a minor wire -// optimization and is pointless if dealing with a -// badly behaved client. -func TestHTTP10ConnectionHeader(t *testing.T) { - defer afterTest(t) - - mux := NewServeMux() - mux.Handle("/", HandlerFunc(func(resp ResponseWriter, req *Request) {})) - ts := httptest.NewServer(mux) - defer ts.Close() - - // net/http uses HTTP/1.1 for requests, so write requests manually - tests := []struct { - req string // raw http request - expect []string // expected Connection header(s) - }{ - { - req: "GET / HTTP/1.0\r\n\r\n", - expect: nil, - }, - { - req: "OPTIONS * HTTP/1.0\r\n\r\n", - expect: nil, - }, - { - req: "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n", - expect: []string{"keep-alive"}, - }, - } - - for _, tt := range tests { - conn, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatal("dial err:", err) - } - - _, err = fmt.Fprint(conn, tt.req) - if err != nil { - t.Fatal("conn write err:", err) - } - - resp, err := ReadResponse(bufio.NewReader(conn), &Request{Method: "GET"}) - if err != nil { - t.Fatal("ReadResponse err:", err) - } - conn.Close() - resp.Body.Close() - - got := resp.Header["Connection"] - if !reflect.DeepEqual(got, tt.expect) { - t.Errorf("wrong Connection headers for request %q. Got %q expect %q", tt.req, got, tt.expect) - } - } -} - -// See golang.org/issue/5660 -func TestServerReaderFromOrder(t *testing.T) { - defer afterTest(t) - pr, pw := io.Pipe() - const size = 3 << 20 - ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) { - rw.Header().Set("Content-Type", "text/plain") // prevent sniffing path - done := make(chan bool) - go func() { - io.Copy(rw, pr) - close(done) - }() - time.Sleep(25 * time.Millisecond) // give Copy a chance to break things - n, err := io.Copy(ioutil.Discard, req.Body) - if err != nil { - t.Errorf("handler Copy: %v", err) - return - } - if n != size { - t.Errorf("handler Copy = %d; want %d", n, size) - } - pw.Write([]byte("hi")) - pw.Close() - <-done - })) - defer ts.Close() - - req, err := NewRequest("POST", ts.URL, io.LimitReader(neverEnding('a'), size)) - if err != nil { - t.Fatal(err) - } - res, err := DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - all, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Fatal(err) - } - res.Body.Close() - if string(all) != "hi" { - t.Errorf("Body = %q; want hi", all) - } -} - -// Issue 6157, Issue 6685 -func TestCodesPreventingContentTypeAndBody(t *testing.T) { - for _, code := range []int{StatusNotModified, StatusNoContent, StatusContinue} { - ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) { - if r.URL.Path == "/header" { - w.Header().Set("Content-Length", "123") - } - w.WriteHeader(code) - if r.URL.Path == "/more" { - w.Write([]byte("stuff")) - } - })) - for _, req := range []string{ - "GET / HTTP/1.0", - "GET /header HTTP/1.0", - "GET /more HTTP/1.0", - "GET / HTTP/1.1", - "GET /header HTTP/1.1", - "GET /more HTTP/1.1", - } { - got := ht.rawResponse(req) - wantStatus := fmt.Sprintf("%d %s", code, StatusText(code)) - if !strings.Contains(got, wantStatus) { - t.Errorf("Code %d: Wanted %q Modified for %q: %s", code, wantStatus, req, got) - } else if strings.Contains(got, "Content-Length") { - t.Errorf("Code %d: Got a Content-Length from %q: %s", code, req, got) - } else if strings.Contains(got, "stuff") { - t.Errorf("Code %d: Response contains a body from %q: %s", code, req, got) - } - } - } -} - -func TestContentTypeOkayOn204(t *testing.T) { - ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) { - w.Header().Set("Content-Length", "123") // suppressed - w.Header().Set("Content-Type", "foo/bar") - w.WriteHeader(204) - })) - got := ht.rawResponse("GET / HTTP/1.1") - if !strings.Contains(got, "Content-Type: foo/bar") { - t.Errorf("Response = %q; want Content-Type: foo/bar", got) - } - if strings.Contains(got, "Content-Length: 123") { - t.Errorf("Response = %q; don't want a Content-Length", got) - } -} - -// Issue 6995 -// A server Handler can receive a Request, and then turn around and -// give a copy of that Request.Body out to the Transport (e.g. any -// proxy). So then two people own that Request.Body (both the server -// and the http client), and both think they can close it on failure. -// Therefore, all incoming server requests Bodies need to be thread-safe. -func TestTransportAndServerSharedBodyRace(t *testing.T) { - defer afterTest(t) - - const bodySize = 1 << 20 - - unblockBackend := make(chan bool) - backend := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) { - io.CopyN(rw, req.Body, bodySize/2) - <-unblockBackend - })) - defer backend.Close() - - backendRespc := make(chan *Response, 1) - proxy := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) { - if req.RequestURI == "/foo" { - rw.Write([]byte("bar")) - return - } - req2, _ := NewRequest("POST", backend.URL, req.Body) - req2.ContentLength = bodySize - - bresp, err := DefaultClient.Do(req2) - if err != nil { - t.Errorf("Proxy outbound request: %v", err) - return - } - _, err = io.CopyN(ioutil.Discard, bresp.Body, bodySize/4) - if err != nil { - t.Errorf("Proxy copy error: %v", err) - return - } - backendRespc <- bresp // to close later - - // Try to cause a race: Both the DefaultTransport and the proxy handler's Server - // will try to read/close req.Body (aka req2.Body) - DefaultTransport.(*Transport).CancelRequest(req2) - rw.Write([]byte("OK")) - })) - defer proxy.Close() - - req, _ := NewRequest("POST", proxy.URL, io.LimitReader(neverEnding('a'), bodySize)) - res, err := DefaultClient.Do(req) - if err != nil { - t.Fatalf("Original request: %v", err) - } - - // Cleanup, so we don't leak goroutines. - res.Body.Close() - close(unblockBackend) - (<-backendRespc).Body.Close() -} - -// Test that a hanging Request.Body.Read from another goroutine can't -// cause the Handler goroutine's Request.Body.Close to block. -func TestRequestBodyCloseDoesntBlock(t *testing.T) { - t.Skipf("Skipping known issue; see golang.org/issue/7121") - if testing.Short() { - t.Skip("skipping in -short mode") - } - defer afterTest(t) - - readErrCh := make(chan error, 1) - errCh := make(chan error, 2) - - server := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) { - go func(body io.Reader) { - _, err := body.Read(make([]byte, 100)) - readErrCh <- err - }(req.Body) - time.Sleep(500 * time.Millisecond) - })) - defer server.Close() - - closeConn := make(chan bool) - defer close(closeConn) - go func() { - conn, err := net.Dial("tcp", server.Listener.Addr().String()) - if err != nil { - errCh <- err - return - } - defer conn.Close() - _, err = conn.Write([]byte("POST / HTTP/1.1\r\nConnection: close\r\nHost: foo\r\nContent-Length: 100000\r\n\r\n")) - if err != nil { - errCh <- err - return - } - // And now just block, making the server block on our - // 100000 bytes of body that will never arrive. - <-closeConn - }() - select { - case err := <-readErrCh: - if err == nil { - t.Error("Read was nil. Expected error.") - } - case err := <-errCh: - t.Error(err) - case <-time.After(5 * time.Second): - t.Error("timeout") - } -} - -func TestResponseWriterWriteStringAllocs(t *testing.T) { - ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) { - if r.URL.Path == "/s" { - io.WriteString(w, "Hello world") - } else { - w.Write([]byte("Hello world")) - } - })) - before := testing.AllocsPerRun(50, func() { ht.rawResponse("GET / HTTP/1.0") }) - after := testing.AllocsPerRun(50, func() { ht.rawResponse("GET /s HTTP/1.0") }) - if int(after) >= int(before) { - t.Errorf("WriteString allocs of %v >= Write allocs of %v", after, before) - } -} - -func TestAppendTime(t *testing.T) { - var b [len(TimeFormat)]byte - t1 := time.Date(2013, 9, 21, 15, 41, 0, 0, time.FixedZone("CEST", 2*60*60)) - res := ExportAppendTime(b[:0], t1) - t2, err := ParseTime(string(res)) - if err != nil { - t.Fatalf("Error parsing time: %s", err) - } - if !t1.Equal(t2) { - t.Fatalf("Times differ; expected: %v, got %v (%s)", t1, t2, string(res)) - } -} - -func TestServerConnState(t *testing.T) { - defer afterTest(t) - handler := map[string]func(w ResponseWriter, r *Request){ - "/": func(w ResponseWriter, r *Request) { - fmt.Fprintf(w, "Hello.") - }, - "/close": func(w ResponseWriter, r *Request) { - w.Header().Set("Connection", "close") - fmt.Fprintf(w, "Hello.") - }, - "/hijack": func(w ResponseWriter, r *Request) { - c, _, _ := w.(Hijacker).Hijack() - c.Write([]byte("HTTP/1.0 200 OK\r\nConnection: close\r\n\r\nHello.")) - c.Close() - }, - "/hijack-panic": func(w ResponseWriter, r *Request) { - c, _, _ := w.(Hijacker).Hijack() - c.Write([]byte("HTTP/1.0 200 OK\r\nConnection: close\r\n\r\nHello.")) - c.Close() - panic("intentional panic") - }, - } - ts := httptest.NewUnstartedServer(HandlerFunc(func(w ResponseWriter, r *Request) { - handler[r.URL.Path](w, r) - })) - defer ts.Close() - - var mu sync.Mutex // guard stateLog and connID - var stateLog = map[int][]ConnState{} - var connID = map[net.Conn]int{} - - ts.Config.ErrorLog = log.New(ioutil.Discard, "", 0) - ts.Config.ConnState = func(c net.Conn, state ConnState) { - if c == nil { - t.Errorf("nil conn seen in state %s", state) - return - } - mu.Lock() - defer mu.Unlock() - id, ok := connID[c] - if !ok { - id = len(connID) + 1 - connID[c] = id - } - stateLog[id] = append(stateLog[id], state) - } - ts.Start() - - mustGet(t, ts.URL+"/") - mustGet(t, ts.URL+"/close") - - mustGet(t, ts.URL+"/") - mustGet(t, ts.URL+"/", "Connection", "close") - - mustGet(t, ts.URL+"/hijack") - mustGet(t, ts.URL+"/hijack-panic") - - // New->Closed - { - c, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatal(err) - } - c.Close() - } - - // New->Active->Closed - { - c, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatal(err) - } - if _, err := io.WriteString(c, "BOGUS REQUEST\r\n\r\n"); err != nil { - t.Fatal(err) - } - c.Close() - } - - // New->Idle->Closed - { - c, err := net.Dial("tcp", ts.Listener.Addr().String()) - if err != nil { - t.Fatal(err) - } - if _, err := io.WriteString(c, "GET / HTTP/1.1\r\nHost: foo\r\n\r\n"); err != nil { - t.Fatal(err) - } - res, err := ReadResponse(bufio.NewReader(c), nil) - if err != nil { - t.Fatal(err) - } - if _, err := io.Copy(ioutil.Discard, res.Body); err != nil { - t.Fatal(err) - } - c.Close() - } - - want := map[int][]ConnState{ - 1: []ConnState{StateNew, StateActive, StateIdle, StateActive, StateClosed}, - 2: []ConnState{StateNew, StateActive, StateIdle, StateActive, StateClosed}, - 3: []ConnState{StateNew, StateActive, StateHijacked}, - 4: []ConnState{StateNew, StateActive, StateHijacked}, - 5: []ConnState{StateNew, StateClosed}, - 6: []ConnState{StateNew, StateActive, StateClosed}, - 7: []ConnState{StateNew, StateActive, StateIdle, StateClosed}, - } - logString := func(m map[int][]ConnState) string { - var b bytes.Buffer - for id, l := range m { - fmt.Fprintf(&b, "Conn %d: ", id) - for _, s := range l { - fmt.Fprintf(&b, "%s ", s) - } - b.WriteString("\n") - } - return b.String() - } - - for i := 0; i < 5; i++ { - time.Sleep(time.Duration(i) * 50 * time.Millisecond) - mu.Lock() - match := reflect.DeepEqual(stateLog, want) - mu.Unlock() - if match { - return - } - } - - mu.Lock() - t.Errorf("Unexpected events.\nGot log: %s\n Want: %s\n", logString(stateLog), logString(want)) - mu.Unlock() -} - -func mustGet(t *testing.T, url string, headers ...string) { - req, err := NewRequest("GET", url, nil) - if err != nil { - t.Fatal(err) - } - for len(headers) > 0 { - req.Header.Add(headers[0], headers[1]) - headers = headers[2:] - } - res, err := DefaultClient.Do(req) - if err != nil { - t.Errorf("Error fetching %s: %v", url, err) - return - } - _, err = ioutil.ReadAll(res.Body) - defer res.Body.Close() - if err != nil { - t.Errorf("Error reading %s: %v", url, err) - } -} - -func TestServerKeepAlivesEnabled(t *testing.T) { - defer afterTest(t) - ts := httptest.NewUnstartedServer(HandlerFunc(func(w ResponseWriter, r *Request) {})) - ts.Config.SetKeepAlivesEnabled(false) - ts.Start() - defer ts.Close() - res, err := Get(ts.URL) - if err != nil { - t.Fatal(err) - } - defer res.Body.Close() - if !res.Close { - t.Errorf("Body.Close == false; want true") - } -} - -// golang.org/issue/7856 -func TestServerEmptyBodyRace(t *testing.T) { - defer afterTest(t) - var n int32 - ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) { - atomic.AddInt32(&n, 1) - })) - defer ts.Close() - var wg sync.WaitGroup - const reqs = 20 - for i := 0; i < reqs; i++ { - wg.Add(1) - go func() { - defer wg.Done() - res, err := Get(ts.URL) - if err != nil { - t.Error(err) - return - } - defer res.Body.Close() - _, err = io.Copy(ioutil.Discard, res.Body) - if err != nil { - t.Error(err) - return - } - }() - } - wg.Wait() - if got := atomic.LoadInt32(&n); got != reqs { - t.Errorf("handler ran %d times; want %d", got, reqs) - } -} - -func TestServerConnStateNew(t *testing.T) { - sawNew := false // if the test is buggy, we'll race on this variable. - srv := &Server{ - ConnState: func(c net.Conn, state ConnState) { - if state == StateNew { - sawNew = true // testing that this write isn't racy - } - }, - Handler: HandlerFunc(func(w ResponseWriter, r *Request) {}), // irrelevant - } - srv.Serve(&oneConnListener{ - conn: &rwTestConn{ - Reader: strings.NewReader("GET / HTTP/1.1\r\nHost: foo\r\n\r\n"), - Writer: ioutil.Discard, - }, - }) - if !sawNew { // testing that this read isn't racy - t.Error("StateNew not seen") - } -} - -func BenchmarkClientServer(b *testing.B) { - b.ReportAllocs() - b.StopTimer() - ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) { - fmt.Fprintf(rw, "Hello world.\n") - })) - defer ts.Close() - b.StartTimer() - - for i := 0; i < b.N; i++ { - res, err := Get(ts.URL) - if err != nil { - b.Fatal("Get:", err) - } - all, err := ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - b.Fatal("ReadAll:", err) - } - body := string(all) - if body != "Hello world.\n" { - b.Fatal("Got body:", body) - } - } - - b.StopTimer() -} - -// A benchmark for profiling the server without the HTTP client code. -// The client code runs in a subprocess. -// -// For use like: -// $ go test -c -// $ ./http.test -test.run=XX -test.bench=BenchmarkServer -test.benchtime=15s -test.cpuprofile=http.prof -// $ go tool pprof http.test http.prof -// (pprof) web -func BenchmarkServer(b *testing.B) { - b.ReportAllocs() - // Child process mode; - if url := os.Getenv("TEST_BENCH_SERVER_URL"); url != "" { - n, err := strconv.Atoi(os.Getenv("TEST_BENCH_CLIENT_N")) - if err != nil { - panic(err) - } - for i := 0; i < n; i++ { - res, err := Get(url) - if err != nil { - log.Panicf("Get: %v", err) - } - all, err := ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - log.Panicf("ReadAll: %v", err) - } - body := string(all) - if body != "Hello world.\n" { - log.Panicf("Got body: %q", body) - } - } - os.Exit(0) - return - } - - var res = []byte("Hello world.\n") - b.StopTimer() - ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) { - rw.Header().Set("Content-Type", "text/html; charset=utf-8") - rw.Write(res) - })) - defer ts.Close() - b.StartTimer() - - cmd := exec.Command(os.Args[0], "-test.run=XXXX", "-test.bench=BenchmarkServer") - cmd.Env = append([]string{ - fmt.Sprintf("TEST_BENCH_CLIENT_N=%d", b.N), - fmt.Sprintf("TEST_BENCH_SERVER_URL=%s", ts.URL), - }, os.Environ()...) - out, err := cmd.CombinedOutput() - if err != nil { - b.Errorf("Test failure: %v, with output: %s", err, out) - } -} - -func BenchmarkServerFakeConnNoKeepAlive(b *testing.B) { - b.ReportAllocs() - req := reqBytes(`GET / HTTP/1.0 -Host: golang.org -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.52 Safari/537.17 -Accept-Encoding: gzip,deflate,sdch -Accept-Language: en-US,en;q=0.8 -Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3 -`) - res := []byte("Hello world!\n") - - conn := &testConn{ - // testConn.Close will not push into the channel - // if it's full. - closec: make(chan bool, 1), - } - handler := HandlerFunc(func(rw ResponseWriter, r *Request) { - rw.Header().Set("Content-Type", "text/html; charset=utf-8") - rw.Write(res) - }) - ln := new(oneConnListener) - for i := 0; i < b.N; i++ { - conn.readBuf.Reset() - conn.writeBuf.Reset() - conn.readBuf.Write(req) - ln.conn = conn - Serve(ln, handler) - <-conn.closec - } -} - -// repeatReader reads content count times, then EOFs. -type repeatReader struct { - content []byte - count int - off int -} - -func (r *repeatReader) Read(p []byte) (n int, err error) { - if r.count <= 0 { - return 0, io.EOF - } - n = copy(p, r.content[r.off:]) - r.off += n - if r.off == len(r.content) { - r.count-- - r.off = 0 - } - return -} - -func BenchmarkServerFakeConnWithKeepAlive(b *testing.B) { - b.ReportAllocs() - - req := reqBytes(`GET / HTTP/1.1 -Host: golang.org -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 -User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.52 Safari/537.17 -Accept-Encoding: gzip,deflate,sdch -Accept-Language: en-US,en;q=0.8 -Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3 -`) - res := []byte("Hello world!\n") - - conn := &rwTestConn{ - Reader: &repeatReader{content: req, count: b.N}, - Writer: ioutil.Discard, - closec: make(chan bool, 1), - } - handled := 0 - handler := HandlerFunc(func(rw ResponseWriter, r *Request) { - handled++ - rw.Header().Set("Content-Type", "text/html; charset=utf-8") - rw.Write(res) - }) - ln := &oneConnListener{conn: conn} - go Serve(ln, handler) - <-conn.closec - if b.N != handled { - b.Errorf("b.N=%d but handled %d", b.N, handled) - } -} - -// same as above, but representing the most simple possible request -// and handler. Notably: the handler does not call rw.Header(). -func BenchmarkServerFakeConnWithKeepAliveLite(b *testing.B) { - b.ReportAllocs() - - req := reqBytes(`GET / HTTP/1.1 -Host: golang.org -`) - res := []byte("Hello world!\n") - - conn := &rwTestConn{ - Reader: &repeatReader{content: req, count: b.N}, - Writer: ioutil.Discard, - closec: make(chan bool, 1), - } - handled := 0 - handler := HandlerFunc(func(rw ResponseWriter, r *Request) { - handled++ - rw.Write(res) - }) - ln := &oneConnListener{conn: conn} - go Serve(ln, handler) - <-conn.closec - if b.N != handled { - b.Errorf("b.N=%d but handled %d", b.N, handled) - } -} - -const someResponse = "some response" - -// A Response that's just no bigger than 2KB, the buffer-before-chunking threshold. -var response = bytes.Repeat([]byte(someResponse), 2<<10/len(someResponse)) - -// Both Content-Type and Content-Length set. Should be no buffering. -func BenchmarkServerHandlerTypeLen(b *testing.B) { - benchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) { - w.Header().Set("Content-Type", "text/html") - w.Header().Set("Content-Length", strconv.Itoa(len(response))) - w.Write(response) - })) -} - -// A Content-Type is set, but no length. No sniffing, but will count the Content-Length. -func BenchmarkServerHandlerNoLen(b *testing.B) { - benchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) { - w.Header().Set("Content-Type", "text/html") - w.Write(response) - })) -} - -// A Content-Length is set, but the Content-Type will be sniffed. -func BenchmarkServerHandlerNoType(b *testing.B) { - benchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) { - w.Header().Set("Content-Length", strconv.Itoa(len(response))) - w.Write(response) - })) -} - -// Neither a Content-Type or Content-Length, so sniffed and counted. -func BenchmarkServerHandlerNoHeader(b *testing.B) { - benchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) { - w.Write(response) - })) -} - -func benchmarkHandler(b *testing.B, h Handler) { - b.ReportAllocs() - req := reqBytes(`GET / HTTP/1.1 -Host: golang.org -`) - conn := &rwTestConn{ - Reader: &repeatReader{content: req, count: b.N}, - Writer: ioutil.Discard, - closec: make(chan bool, 1), - } - handled := 0 - handler := HandlerFunc(func(rw ResponseWriter, r *Request) { - handled++ - h.ServeHTTP(rw, r) - }) - ln := &oneConnListener{conn: conn} - go Serve(ln, handler) - <-conn.closec - if b.N != handled { - b.Errorf("b.N=%d but handled %d", b.N, handled) - } -} - -func BenchmarkServerHijack(b *testing.B) { - b.ReportAllocs() - req := reqBytes(`GET / HTTP/1.1 -Host: golang.org -`) - h := HandlerFunc(func(w ResponseWriter, r *Request) { - conn, _, err := w.(Hijacker).Hijack() - if err != nil { - panic(err) - } - conn.Close() - }) - conn := &rwTestConn{ - Writer: ioutil.Discard, - closec: make(chan bool, 1), - } - ln := &oneConnListener{conn: conn} - for i := 0; i < b.N; i++ { - conn.Reader = bytes.NewReader(req) - ln.conn = conn - Serve(ln, h) - <-conn.closec - } -} === modified file 'http13client/server.go' --- http13client/server.go 2014-06-20 11:00:47 +0000 +++ http13client/server.go 2014-06-20 12:05:53 +0000 @@ -2,1976 +2,16 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// HTTP server. See RFC 2616. - package http import ( - "bufio" - "crypto/tls" - "errors" "fmt" "io" "io/ioutil" "log" "net" - "net/url" - "os" - "path" - "runtime" - "strconv" - "strings" "sync" - "sync/atomic" - "time" -) - -// Errors introduced by the HTTP server. -var ( - ErrWriteAfterFlush = errors.New("Conn.Write called after Flush") - ErrBodyNotAllowed = errors.New("http: request method or response status code does not allow body") - ErrHijacked = errors.New("Conn has been hijacked") - ErrContentLength = errors.New("Conn.Write wrote more than the declared Content-Length") -) - -// Objects implementing the Handler interface can be -// registered to serve a particular path or subtree -// in the HTTP server. -// -// ServeHTTP should write reply headers and data to the ResponseWriter -// and then return. Returning signals that the request is finished -// and that the HTTP server can move on to the next request on -// the connection. -type Handler interface { - ServeHTTP(ResponseWriter, *Request) -} - -// A ResponseWriter interface is used by an HTTP handler to -// construct an HTTP response. -type ResponseWriter interface { - // Header returns the header map that will be sent by WriteHeader. - // Changing the header after a call to WriteHeader (or Write) has - // no effect. - Header() Header - - // Write writes the data to the connection as part of an HTTP reply. - // If WriteHeader has not yet been called, Write calls WriteHeader(http.StatusOK) - // before writing the data. If the Header does not contain a - // Content-Type line, Write adds a Content-Type set to the result of passing - // the initial 512 bytes of written data to DetectContentType. - Write([]byte) (int, error) - - // WriteHeader sends an HTTP response header with status code. - // If WriteHeader is not called explicitly, the first call to Write - // will trigger an implicit WriteHeader(http.StatusOK). - // Thus explicit calls to WriteHeader are mainly used to - // send error codes. - WriteHeader(int) -} - -// The Flusher interface is implemented by ResponseWriters that allow -// an HTTP handler to flush buffered data to the client. -// -// Note that even for ResponseWriters that support Flush, -// if the client is connected through an HTTP proxy, -// the buffered data may not reach the client until the response -// completes. -type Flusher interface { - // Flush sends any buffered data to the client. - Flush() -} - -// The Hijacker interface is implemented by ResponseWriters that allow -// an HTTP handler to take over the connection. -type Hijacker interface { - // Hijack lets the caller take over the connection. - // After a call to Hijack(), the HTTP server library - // will not do anything else with the connection. - // It becomes the caller's responsibility to manage - // and close the connection. - Hijack() (net.Conn, *bufio.ReadWriter, error) -} - -// The CloseNotifier interface is implemented by ResponseWriters which -// allow detecting when the underlying connection has gone away. -// -// This mechanism can be used to cancel long operations on the server -// if the client has disconnected before the response is ready. -type CloseNotifier interface { - // CloseNotify returns a channel that receives a single value - // when the client connection has gone away. - CloseNotify() <-chan bool -} - -// A conn represents the server side of an HTTP connection. -type conn struct { - remoteAddr string // network address of remote side - server *Server // the Server on which the connection arrived - rwc net.Conn // i/o connection - sr liveSwitchReader // where the LimitReader reads from; usually the rwc - lr *io.LimitedReader // io.LimitReader(sr) - buf *bufio.ReadWriter // buffered(lr,rwc), reading from bufio->limitReader->sr->rwc - tlsState *tls.ConnectionState // or nil when not using TLS - - mu sync.Mutex // guards the following - clientGone bool // if client has disconnected mid-request - closeNotifyc chan bool // made lazily - hijackedv bool // connection has been hijacked by handler -} - -func (c *conn) hijacked() bool { - c.mu.Lock() - defer c.mu.Unlock() - return c.hijackedv -} - -func (c *conn) hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error) { - c.mu.Lock() - defer c.mu.Unlock() - if c.hijackedv { - return nil, nil, ErrHijacked - } - if c.closeNotifyc != nil { - return nil, nil, errors.New("http: Hijack is incompatible with use of CloseNotifier") - } - c.hijackedv = true - rwc = c.rwc - buf = c.buf - c.rwc = nil - c.buf = nil - c.setState(rwc, StateHijacked) - return -} - -func (c *conn) closeNotify() <-chan bool { - c.mu.Lock() - defer c.mu.Unlock() - if c.closeNotifyc == nil { - c.closeNotifyc = make(chan bool, 1) - if c.hijackedv { - // to obey the function signature, even though - // it'll never receive a value. - return c.closeNotifyc - } - pr, pw := io.Pipe() - - readSource := c.sr.r - c.sr.Lock() - c.sr.r = pr - c.sr.Unlock() - go func() { - _, err := io.Copy(pw, readSource) - if err == nil { - err = io.EOF - } - pw.CloseWithError(err) - c.noteClientGone() - }() - } - return c.closeNotifyc -} - -func (c *conn) noteClientGone() { - c.mu.Lock() - defer c.mu.Unlock() - if c.closeNotifyc != nil && !c.clientGone { - c.closeNotifyc <- true - } - c.clientGone = true -} - -// A switchReader can have its Reader changed at runtime. -// It's not safe for concurrent Reads and switches. -type switchReader struct { - io.Reader -} - -// A switchWriter can have its Writer changed at runtime. -// It's not safe for concurrent Writes and switches. -type switchWriter struct { - io.Writer -} - -// A liveSwitchReader is a switchReader that's safe for concurrent -// reads and switches, if its mutex is held. -type liveSwitchReader struct { - sync.Mutex - r io.Reader -} - -func (sr *liveSwitchReader) Read(p []byte) (n int, err error) { - sr.Lock() - r := sr.r - sr.Unlock() - return r.Read(p) -} - -// This should be >= 512 bytes for DetectContentType, -// but otherwise it's somewhat arbitrary. -const bufferBeforeChunkingSize = 2048 - -// chunkWriter writes to a response's conn buffer, and is the writer -// wrapped by the response.bufw buffered writer. -// -// chunkWriter also is responsible for finalizing the Header, including -// conditionally setting the Content-Type and setting a Content-Length -// in cases where the handler's final output is smaller than the buffer -// size. It also conditionally adds chunk headers, when in chunking mode. -// -// See the comment above (*response).Write for the entire write flow. -type chunkWriter struct { - res *response - - // header is either nil or a deep clone of res.handlerHeader - // at the time of res.WriteHeader, if res.WriteHeader is - // called and extra buffering is being done to calculate - // Content-Type and/or Content-Length. - header Header - - // wroteHeader tells whether the header's been written to "the - // wire" (or rather: w.conn.buf). this is unlike - // (*response).wroteHeader, which tells only whether it was - // logically written. - wroteHeader bool - - // set by the writeHeader method: - chunking bool // using chunked transfer encoding for reply body -} - -var ( - crlf = []byte("\r\n") - colonSpace = []byte(": ") -) - -func (cw *chunkWriter) Write(p []byte) (n int, err error) { - if !cw.wroteHeader { - cw.writeHeader(p) - } - if cw.res.req.Method == "HEAD" { - // Eat writes. - return len(p), nil - } - if cw.chunking { - _, err = fmt.Fprintf(cw.res.conn.buf, "%x\r\n", len(p)) - if err != nil { - cw.res.conn.rwc.Close() - return - } - } - n, err = cw.res.conn.buf.Write(p) - if cw.chunking && err == nil { - _, err = cw.res.conn.buf.Write(crlf) - } - if err != nil { - cw.res.conn.rwc.Close() - } - return -} - -func (cw *chunkWriter) flush() { - if !cw.wroteHeader { - cw.writeHeader(nil) - } - cw.res.conn.buf.Flush() -} - -func (cw *chunkWriter) close() { - if !cw.wroteHeader { - cw.writeHeader(nil) - } - if cw.chunking { - // zero EOF chunk, trailer key/value pairs (currently - // unsupported in Go's server), followed by a blank - // line. - cw.res.conn.buf.WriteString("0\r\n\r\n") - } -} - -// A response represents the server side of an HTTP response. -type response struct { - conn *conn - req *Request // request for this response - wroteHeader bool // reply header has been (logically) written - wroteContinue bool // 100 Continue response was written - - w *bufio.Writer // buffers output in chunks to chunkWriter - cw chunkWriter - sw *switchWriter // of the bufio.Writer, for return to putBufioWriter - - // handlerHeader is the Header that Handlers get access to, - // which may be retained and mutated even after WriteHeader. - // handlerHeader is copied into cw.header at WriteHeader - // time, and privately mutated thereafter. - handlerHeader Header - calledHeader bool // handler accessed handlerHeader via Header - - written int64 // number of bytes written in body - contentLength int64 // explicitly-declared Content-Length; or -1 - status int // status code passed to WriteHeader - - // close connection after this reply. set on request and - // updated after response from handler if there's a - // "Connection: keep-alive" response header and a - // Content-Length. - closeAfterReply bool - - // requestBodyLimitHit is set by requestTooLarge when - // maxBytesReader hits its max size. It is checked in - // WriteHeader, to make sure we don't consume the - // remaining request body to try to advance to the next HTTP - // request. Instead, when this is set, we stop reading - // subsequent requests on this connection and stop reading - // input from it. - requestBodyLimitHit bool - - handlerDone bool // set true when the handler exits - - // Buffers for Date and Content-Length - dateBuf [len(TimeFormat)]byte - clenBuf [10]byte -} - -// requestTooLarge is called by maxBytesReader when too much input has -// been read from the client. -func (w *response) requestTooLarge() { - w.closeAfterReply = true - w.requestBodyLimitHit = true - if !w.wroteHeader { - w.Header().Set("Connection", "close") - } -} - -// needsSniff reports whether a Content-Type still needs to be sniffed. -func (w *response) needsSniff() bool { - _, haveType := w.handlerHeader["Content-Type"] - return !w.cw.wroteHeader && !haveType && w.written < sniffLen -} - -// writerOnly hides an io.Writer value's optional ReadFrom method -// from io.Copy. -type writerOnly struct { - io.Writer -} - -func srcIsRegularFile(src io.Reader) (isRegular bool, err error) { - switch v := src.(type) { - case *os.File: - fi, err := v.Stat() - if err != nil { - return false, err - } - return fi.Mode().IsRegular(), nil - case *io.LimitedReader: - return srcIsRegularFile(v.R) - default: - return - } -} - -// ReadFrom is here to optimize copying from an *os.File regular file -// to a *net.TCPConn with sendfile. -func (w *response) ReadFrom(src io.Reader) (n int64, err error) { - // Our underlying w.conn.rwc is usually a *TCPConn (with its - // own ReadFrom method). If not, or if our src isn't a regular - // file, just fall back to the normal copy method. - rf, ok := w.conn.rwc.(io.ReaderFrom) - regFile, err := srcIsRegularFile(src) - if err != nil { - return 0, err - } - if !ok || !regFile { - return io.Copy(writerOnly{w}, src) - } - - // sendfile path: - - if !w.wroteHeader { - w.WriteHeader(StatusOK) - } - - if w.needsSniff() { - n0, err := io.Copy(writerOnly{w}, io.LimitReader(src, sniffLen)) - n += n0 - if err != nil { - return n, err - } - } - - w.w.Flush() // get rid of any previous writes - w.cw.flush() // make sure Header is written; flush data to rwc - - // Now that cw has been flushed, its chunking field is guaranteed initialized. - if !w.cw.chunking && w.bodyAllowed() { - n0, err := rf.ReadFrom(src) - n += n0 - w.written += n0 - return n, err - } - - n0, err := io.Copy(writerOnly{w}, src) - n += n0 - return n, err -} - -// noLimit is an effective infinite upper bound for io.LimitedReader -const noLimit int64 = (1 << 63) - 1 - -// debugServerConnections controls whether all server connections are wrapped -// with a verbose logging wrapper. -const debugServerConnections = false - -// Create new connection from rwc. -func (srv *Server) newConn(rwc net.Conn) (c *conn, err error) { - c = new(conn) - c.remoteAddr = rwc.RemoteAddr().String() - c.server = srv - c.rwc = rwc - if debugServerConnections { - c.rwc = newLoggingConn("server", c.rwc) - } - c.sr = liveSwitchReader{r: c.rwc} - c.lr = io.LimitReader(&c.sr, noLimit).(*io.LimitedReader) - br := newBufioReader(c.lr) - bw := newBufioWriterSize(c.rwc, 4<<10) - c.buf = bufio.NewReadWriter(br, bw) - return c, nil -} - -// TODO: use a sync.Cache instead -var ( - bufioReaderCache = make(chan *bufio.Reader, 4) - bufioWriterCache2k = make(chan *bufio.Writer, 4) - bufioWriterCache4k = make(chan *bufio.Writer, 4) -) - -func bufioWriterCache(size int) chan *bufio.Writer { - switch size { - case 2 << 10: - return bufioWriterCache2k - case 4 << 10: - return bufioWriterCache4k - } - return nil -} - -func newBufioReader(r io.Reader) *bufio.Reader { - select { - case p := <-bufioReaderCache: - p.Reset(r) - return p - default: - return bufio.NewReader(r) - } -} - -func putBufioReader(br *bufio.Reader) { - br.Reset(nil) - select { - case bufioReaderCache <- br: - default: - } -} - -func newBufioWriterSize(w io.Writer, size int) *bufio.Writer { - select { - case p := <-bufioWriterCache(size): - p.Reset(w) - return p - default: - return bufio.NewWriterSize(w, size) - } -} - -func putBufioWriter(bw *bufio.Writer) { - bw.Reset(nil) - select { - case bufioWriterCache(bw.Available()) <- bw: - default: - } -} - -// DefaultMaxHeaderBytes is the maximum permitted size of the headers -// in an HTTP request. -// This can be overridden by setting Server.MaxHeaderBytes. -const DefaultMaxHeaderBytes = 1 << 20 // 1 MB - -func (srv *Server) maxHeaderBytes() int { - if srv.MaxHeaderBytes > 0 { - return srv.MaxHeaderBytes - } - return DefaultMaxHeaderBytes -} - -func (srv *Server) initialLimitedReaderSize() int64 { - return int64(srv.maxHeaderBytes()) + 4096 // bufio slop -} - -// wrapper around io.ReaderCloser which on first read, sends an -// HTTP/1.1 100 Continue header -type expectContinueReader struct { - resp *response - readCloser io.ReadCloser - closed bool -} - -func (ecr *expectContinueReader) Read(p []byte) (n int, err error) { - if ecr.closed { - return 0, ErrBodyReadAfterClose - } - if !ecr.resp.wroteContinue && !ecr.resp.conn.hijacked() { - ecr.resp.wroteContinue = true - ecr.resp.conn.buf.WriteString("HTTP/1.1 100 Continue\r\n\r\n") - ecr.resp.conn.buf.Flush() - } - return ecr.readCloser.Read(p) -} - -func (ecr *expectContinueReader) Close() error { - ecr.closed = true - return ecr.readCloser.Close() -} - -// TimeFormat is the time format to use with -// time.Parse and time.Time.Format when parsing -// or generating times in HTTP headers. -// It is like time.RFC1123 but hard codes GMT as the time zone. -const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" - -// appendTime is a non-allocating version of []byte(t.UTC().Format(TimeFormat)) -func appendTime(b []byte, t time.Time) []byte { - const days = "SunMonTueWedThuFriSat" - const months = "JanFebMarAprMayJunJulAugSepOctNovDec" - - t = t.UTC() - yy, mm, dd := t.Date() - hh, mn, ss := t.Clock() - day := days[3*t.Weekday():] - mon := months[3*(mm-1):] - - return append(b, - day[0], day[1], day[2], ',', ' ', - byte('0'+dd/10), byte('0'+dd%10), ' ', - mon[0], mon[1], mon[2], ' ', - byte('0'+yy/1000), byte('0'+(yy/100)%10), byte('0'+(yy/10)%10), byte('0'+yy%10), ' ', - byte('0'+hh/10), byte('0'+hh%10), ':', - byte('0'+mn/10), byte('0'+mn%10), ':', - byte('0'+ss/10), byte('0'+ss%10), ' ', - 'G', 'M', 'T') -} - -var errTooLarge = errors.New("http: request too large") - -// Read next request from connection. -func (c *conn) readRequest() (w *response, err error) { - if c.hijacked() { - return nil, ErrHijacked - } - - if d := c.server.ReadTimeout; d != 0 { - c.rwc.SetReadDeadline(time.Now().Add(d)) - } - if d := c.server.WriteTimeout; d != 0 { - defer func() { - c.rwc.SetWriteDeadline(time.Now().Add(d)) - }() - } - - c.lr.N = c.server.initialLimitedReaderSize() - var req *Request - if req, err = ReadRequest(c.buf.Reader); err != nil { - if c.lr.N == 0 { - return nil, errTooLarge - } - return nil, err - } - c.lr.N = noLimit - - req.RemoteAddr = c.remoteAddr - req.TLS = c.tlsState - - w = &response{ - conn: c, - req: req, - handlerHeader: make(Header), - contentLength: -1, - } - w.cw.res = w - w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize) - return w, nil -} - -func (w *response) Header() Header { - if w.cw.header == nil && w.wroteHeader && !w.cw.wroteHeader { - // Accessing the header between logically writing it - // and physically writing it means we need to allocate - // a clone to snapshot the logically written state. - w.cw.header = w.handlerHeader.clone() - } - w.calledHeader = true - return w.handlerHeader -} - -// maxPostHandlerReadBytes is the max number of Request.Body bytes not -// consumed by a handler that the server will read from the client -// in order to keep a connection alive. If there are more bytes than -// this then the server to be paranoid instead sends a "Connection: -// close" response. -// -// This number is approximately what a typical machine's TCP buffer -// size is anyway. (if we have the bytes on the machine, we might as -// well read them) -const maxPostHandlerReadBytes = 256 << 10 - -func (w *response) WriteHeader(code int) { - if w.conn.hijacked() { - w.conn.server.logf("http: response.WriteHeader on hijacked connection") - return - } - if w.wroteHeader { - w.conn.server.logf("http: multiple response.WriteHeader calls") - return - } - w.wroteHeader = true - w.status = code - - if w.calledHeader && w.cw.header == nil { - w.cw.header = w.handlerHeader.clone() - } - - if cl := w.handlerHeader.get("Content-Length"); cl != "" { - v, err := strconv.ParseInt(cl, 10, 64) - if err == nil && v >= 0 { - w.contentLength = v - } else { - w.conn.server.logf("http: invalid Content-Length of %q", cl) - w.handlerHeader.Del("Content-Length") - } - } -} - -// extraHeader is the set of headers sometimes added by chunkWriter.writeHeader. -// This type is used to avoid extra allocations from cloning and/or populating -// the response Header map and all its 1-element slices. -type extraHeader struct { - contentType string - connection string - transferEncoding string - date []byte // written if not nil - contentLength []byte // written if not nil -} - -// Sorted the same as extraHeader.Write's loop. -var extraHeaderKeys = [][]byte{ - []byte("Content-Type"), - []byte("Connection"), - []byte("Transfer-Encoding"), -} - -var ( - headerContentLength = []byte("Content-Length: ") - headerDate = []byte("Date: ") -) - -// Write writes the headers described in h to w. -// -// This method has a value receiver, despite the somewhat large size -// of h, because it prevents an allocation. The escape analysis isn't -// smart enough to realize this function doesn't mutate h. -func (h extraHeader) Write(w *bufio.Writer) { - if h.date != nil { - w.Write(headerDate) - w.Write(h.date) - w.Write(crlf) - } - if h.contentLength != nil { - w.Write(headerContentLength) - w.Write(h.contentLength) - w.Write(crlf) - } - for i, v := range []string{h.contentType, h.connection, h.transferEncoding} { - if v != "" { - w.Write(extraHeaderKeys[i]) - w.Write(colonSpace) - w.WriteString(v) - w.Write(crlf) - } - } -} - -// writeHeader finalizes the header sent to the client and writes it -// to cw.res.conn.buf. -// -// p is not written by writeHeader, but is the first chunk of the body -// that will be written. It is sniffed for a Content-Type if none is -// set explicitly. It's also used to set the Content-Length, if the -// total body size was small and the handler has already finished -// running. -func (cw *chunkWriter) writeHeader(p []byte) { - if cw.wroteHeader { - return - } - cw.wroteHeader = true - - w := cw.res - keepAlivesEnabled := w.conn.server.doKeepAlives() - isHEAD := w.req.Method == "HEAD" - - // header is written out to w.conn.buf below. Depending on the - // state of the handler, we either own the map or not. If we - // don't own it, the exclude map is created lazily for - // WriteSubset to remove headers. The setHeader struct holds - // headers we need to add. - header := cw.header - owned := header != nil - if !owned { - header = w.handlerHeader - } - var excludeHeader map[string]bool - delHeader := func(key string) { - if owned { - header.Del(key) - return - } - if _, ok := header[key]; !ok { - return - } - if excludeHeader == nil { - excludeHeader = make(map[string]bool) - } - excludeHeader[key] = true - } - var setHeader extraHeader - - // If the handler is done but never sent a Content-Length - // response header and this is our first (and last) write, set - // it, even to zero. This helps HTTP/1.0 clients keep their - // "keep-alive" connections alive. - // Exceptions: 304/204/1xx responses never get Content-Length, and if - // it was a HEAD request, we don't know the difference between - // 0 actual bytes and 0 bytes because the handler noticed it - // was a HEAD request and chose not to write anything. So for - // HEAD, the handler should either write the Content-Length or - // write non-zero bytes. If it's actually 0 bytes and the - // handler never looked at the Request.Method, we just don't - // send a Content-Length header. - if w.handlerDone && bodyAllowedForStatus(w.status) && header.get("Content-Length") == "" && (!isHEAD || len(p) > 0) { - w.contentLength = int64(len(p)) - setHeader.contentLength = strconv.AppendInt(cw.res.clenBuf[:0], int64(len(p)), 10) - } - - // If this was an HTTP/1.0 request with keep-alive and we sent a - // Content-Length back, we can make this a keep-alive response ... - if w.req.wantsHttp10KeepAlive() && keepAlivesEnabled { - sentLength := header.get("Content-Length") != "" - if sentLength && header.get("Connection") == "keep-alive" { - w.closeAfterReply = false - } - } - - // Check for a explicit (and valid) Content-Length header. - hasCL := w.contentLength != -1 - - if w.req.wantsHttp10KeepAlive() && (isHEAD || hasCL) { - _, connectionHeaderSet := header["Connection"] - if !connectionHeaderSet { - setHeader.connection = "keep-alive" - } - } else if !w.req.ProtoAtLeast(1, 1) || w.req.wantsClose() { - w.closeAfterReply = true - } - - if header.get("Connection") == "close" || !keepAlivesEnabled { - w.closeAfterReply = true - } - - // Per RFC 2616, we should consume the request body before - // replying, if the handler hasn't already done so. But we - // don't want to do an unbounded amount of reading here for - // DoS reasons, so we only try up to a threshold. - if w.req.ContentLength != 0 && !w.closeAfterReply { - ecr, isExpecter := w.req.Body.(*expectContinueReader) - if !isExpecter || ecr.resp.wroteContinue { - n, _ := io.CopyN(ioutil.Discard, w.req.Body, maxPostHandlerReadBytes+1) - if n >= maxPostHandlerReadBytes { - w.requestTooLarge() - delHeader("Connection") - setHeader.connection = "close" - } else { - w.req.Body.Close() - } - } - } - - code := w.status - if bodyAllowedForStatus(code) { - // If no content type, apply sniffing algorithm to body. - _, haveType := header["Content-Type"] - if !haveType { - setHeader.contentType = DetectContentType(p) - } - } else { - for _, k := range suppressedHeaders(code) { - delHeader(k) - } - } - - if _, ok := header["Date"]; !ok { - setHeader.date = appendTime(cw.res.dateBuf[:0], time.Now()) - } - - te := header.get("Transfer-Encoding") - hasTE := te != "" - if hasCL && hasTE && te != "identity" { - // TODO: return an error if WriteHeader gets a return parameter - // For now just ignore the Content-Length. - w.conn.server.logf("http: WriteHeader called with both Transfer-Encoding of %q and a Content-Length of %d", - te, w.contentLength) - delHeader("Content-Length") - hasCL = false - } - - if w.req.Method == "HEAD" || !bodyAllowedForStatus(code) { - // do nothing - } else if code == StatusNoContent { - delHeader("Transfer-Encoding") - } else if hasCL { - delHeader("Transfer-Encoding") - } else if w.req.ProtoAtLeast(1, 1) { - // HTTP/1.1 or greater: use chunked transfer encoding - // to avoid closing the connection at EOF. - // TODO: this blows away any custom or stacked Transfer-Encoding they - // might have set. Deal with that as need arises once we have a valid - // use case. - cw.chunking = true - setHeader.transferEncoding = "chunked" - } else { - // HTTP version < 1.1: cannot do chunked transfer - // encoding and we don't know the Content-Length so - // signal EOF by closing connection. - w.closeAfterReply = true - delHeader("Transfer-Encoding") // in case already set - } - - // Cannot use Content-Length with non-identity Transfer-Encoding. - if cw.chunking { - delHeader("Content-Length") - } - if !w.req.ProtoAtLeast(1, 0) { - return - } - - if w.closeAfterReply && (!keepAlivesEnabled || !hasToken(cw.header.get("Connection"), "close")) { - delHeader("Connection") - if w.req.ProtoAtLeast(1, 1) { - setHeader.connection = "close" - } - } - - w.conn.buf.WriteString(statusLine(w.req, code)) - cw.header.WriteSubset(w.conn.buf, excludeHeader) - setHeader.Write(w.conn.buf.Writer) - w.conn.buf.Write(crlf) -} - -// statusLines is a cache of Status-Line strings, keyed by code (for -// HTTP/1.1) or negative code (for HTTP/1.0). This is faster than a -// map keyed by struct of two fields. This map's max size is bounded -// by 2*len(statusText), two protocol types for each known official -// status code in the statusText map. -var ( - statusMu sync.RWMutex - statusLines = make(map[int]string) -) - -// statusLine returns a response Status-Line (RFC 2616 Section 6.1) -// for the given request and response status code. -func statusLine(req *Request, code int) string { - // Fast path: - key := code - proto11 := req.ProtoAtLeast(1, 1) - if !proto11 { - key = -key - } - statusMu.RLock() - line, ok := statusLines[key] - statusMu.RUnlock() - if ok { - return line - } - - // Slow path: - proto := "HTTP/1.0" - if proto11 { - proto = "HTTP/1.1" - } - codestring := strconv.Itoa(code) - text, ok := statusText[code] - if !ok { - text = "status code " + codestring - } - line = proto + " " + codestring + " " + text + "\r\n" - if ok { - statusMu.Lock() - defer statusMu.Unlock() - statusLines[key] = line - } - return line -} - -// bodyAllowed returns true if a Write is allowed for this response type. -// It's illegal to call this before the header has been flushed. -func (w *response) bodyAllowed() bool { - if !w.wroteHeader { - panic("") - } - return bodyAllowedForStatus(w.status) -} - -// The Life Of A Write is like this: -// -// Handler starts. No header has been sent. The handler can either -// write a header, or just start writing. Writing before sending a header -// sends an implicitly empty 200 OK header. -// -// If the handler didn't declare a Content-Length up front, we either -// go into chunking mode or, if the handler finishes running before -// the chunking buffer size, we compute a Content-Length and send that -// in the header instead. -// -// Likewise, if the handler didn't set a Content-Type, we sniff that -// from the initial chunk of output. -// -// The Writers are wired together like: -// -// 1. *response (the ResponseWriter) -> -// 2. (*response).w, a *bufio.Writer of bufferBeforeChunkingSize bytes -// 3. chunkWriter.Writer (whose writeHeader finalizes Content-Length/Type) -// and which writes the chunk headers, if needed. -// 4. conn.buf, a bufio.Writer of default (4kB) bytes -// 5. the rwc, the net.Conn. -// -// TODO(bradfitz): short-circuit some of the buffering when the -// initial header contains both a Content-Type and Content-Length. -// Also short-circuit in (1) when the header's been sent and not in -// chunking mode, writing directly to (4) instead, if (2) has no -// buffered data. More generally, we could short-circuit from (1) to -// (3) even in chunking mode if the write size from (1) is over some -// threshold and nothing is in (2). The answer might be mostly making -// bufferBeforeChunkingSize smaller and having bufio's fast-paths deal -// with this instead. -func (w *response) Write(data []byte) (n int, err error) { - return w.write(len(data), data, "") -} - -func (w *response) WriteString(data string) (n int, err error) { - return w.write(len(data), nil, data) -} - -// either dataB or dataS is non-zero. -func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) { - if w.conn.hijacked() { - w.conn.server.logf("http: response.Write on hijacked connection") - return 0, ErrHijacked - } - if !w.wroteHeader { - w.WriteHeader(StatusOK) - } - if lenData == 0 { - return 0, nil - } - if !w.bodyAllowed() { - return 0, ErrBodyNotAllowed - } - - w.written += int64(lenData) // ignoring errors, for errorKludge - if w.contentLength != -1 && w.written > w.contentLength { - return 0, ErrContentLength - } - if dataB != nil { - return w.w.Write(dataB) - } else { - return w.w.WriteString(dataS) - } -} - -func (w *response) finishRequest() { - w.handlerDone = true - - if !w.wroteHeader { - w.WriteHeader(StatusOK) - } - - w.w.Flush() - putBufioWriter(w.w) - w.cw.close() - w.conn.buf.Flush() - - // Close the body (regardless of w.closeAfterReply) so we can - // re-use its bufio.Reader later safely. - w.req.Body.Close() - - if w.req.MultipartForm != nil { - w.req.MultipartForm.RemoveAll() - } - - if w.req.Method != "HEAD" && w.contentLength != -1 && w.bodyAllowed() && w.contentLength != w.written { - // Did not write enough. Avoid getting out of sync. - w.closeAfterReply = true - } -} - -func (w *response) Flush() { - if !w.wroteHeader { - w.WriteHeader(StatusOK) - } - w.w.Flush() - w.cw.flush() -} - -func (c *conn) finalFlush() { - if c.buf != nil { - c.buf.Flush() - - // Steal the bufio.Reader (~4KB worth of memory) and its associated - // reader for a future connection. - putBufioReader(c.buf.Reader) - - // Steal the bufio.Writer (~4KB worth of memory) and its associated - // writer for a future connection. - putBufioWriter(c.buf.Writer) - - c.buf = nil - } -} - -// Close the connection. -func (c *conn) close() { - c.finalFlush() - if c.rwc != nil { - c.rwc.Close() - c.rwc = nil - } -} - -// rstAvoidanceDelay is the amount of time we sleep after closing the -// write side of a TCP connection before closing the entire socket. -// By sleeping, we increase the chances that the client sees our FIN -// and processes its final data before they process the subsequent RST -// from closing a connection with known unread data. -// This RST seems to occur mostly on BSD systems. (And Windows?) -// This timeout is somewhat arbitrary (~latency around the planet). -const rstAvoidanceDelay = 500 * time.Millisecond - -// closeWrite flushes any outstanding data and sends a FIN packet (if -// client is connected via TCP), signalling that we're done. We then -// pause for a bit, hoping the client processes it before `any -// subsequent RST. -// -// See http://golang.org/issue/3595 -func (c *conn) closeWriteAndWait() { - c.finalFlush() - if tcp, ok := c.rwc.(*net.TCPConn); ok { - tcp.CloseWrite() - } - time.Sleep(rstAvoidanceDelay) -} - -// validNPN reports whether the proto is not a blacklisted Next -// Protocol Negotiation protocol. Empty and built-in protocol types -// are blacklisted and can't be overridden with alternate -// implementations. -func validNPN(proto string) bool { - switch proto { - case "", "http/1.1", "http/1.0": - return false - } - return true -} - -func (c *conn) setState(nc net.Conn, state ConnState) { - if hook := c.server.ConnState; hook != nil { - hook(nc, state) - } -} - -// Serve a new connection. -func (c *conn) serve() { - origConn := c.rwc // copy it before it's set nil on Close or Hijack - defer func() { - if err := recover(); err != nil { - const size = 64 << 10 - buf := make([]byte, size) - buf = buf[:runtime.Stack(buf, false)] - c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf) - } - if !c.hijacked() { - c.close() - c.setState(origConn, StateClosed) - } - }() - - if tlsConn, ok := c.rwc.(*tls.Conn); ok { - if d := c.server.ReadTimeout; d != 0 { - c.rwc.SetReadDeadline(time.Now().Add(d)) - } - if d := c.server.WriteTimeout; d != 0 { - c.rwc.SetWriteDeadline(time.Now().Add(d)) - } - if err := tlsConn.Handshake(); err != nil { - c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err) - return - } - c.tlsState = new(tls.ConnectionState) - *c.tlsState = tlsConn.ConnectionState() - if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) { - if fn := c.server.TLSNextProto[proto]; fn != nil { - h := initNPNRequest{tlsConn, serverHandler{c.server}} - fn(c.server, tlsConn, h) - } - return - } - } - - for { - w, err := c.readRequest() - if c.lr.N != c.server.initialLimitedReaderSize() { - // If we read any bytes off the wire, we're active. - c.setState(c.rwc, StateActive) - } - if err != nil { - if err == errTooLarge { - // Their HTTP client may or may not be - // able to read this if we're - // responding to them and hanging up - // while they're still writing their - // request. Undefined behavior. - io.WriteString(c.rwc, "HTTP/1.1 413 Request Entity Too Large\r\n\r\n") - c.closeWriteAndWait() - break - } else if err == io.EOF { - break // Don't reply - } else if neterr, ok := err.(net.Error); ok && neterr.Timeout() { - break // Don't reply - } - io.WriteString(c.rwc, "HTTP/1.1 400 Bad Request\r\n\r\n") - break - } - - // Expect 100 Continue support - req := w.req - if req.expectsContinue() { - if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 { - // Wrap the Body reader with one that replies on the connection - req.Body = &expectContinueReader{readCloser: req.Body, resp: w} - } - req.Header.Del("Expect") - } else if req.Header.get("Expect") != "" { - w.sendExpectationFailed() - break - } - - // HTTP cannot have multiple simultaneous active requests.[*] - // Until the server replies to this request, it can't read another, - // so we might as well run the handler in this goroutine. - // [*] Not strictly true: HTTP pipelining. We could let them all process - // in parallel even if their responses need to be serialized. - serverHandler{c.server}.ServeHTTP(w, w.req) - if c.hijacked() { - return - } - w.finishRequest() - if w.closeAfterReply { - if w.requestBodyLimitHit { - c.closeWriteAndWait() - } - break - } - c.setState(c.rwc, StateIdle) - } -} - -func (w *response) sendExpectationFailed() { - // TODO(bradfitz): let ServeHTTP handlers handle - // requests with non-standard expectation[s]? Seems - // theoretical at best, and doesn't fit into the - // current ServeHTTP model anyway. We'd need to - // make the ResponseWriter an optional - // "ExpectReplier" interface or something. - // - // For now we'll just obey RFC 2616 14.20 which says - // "If a server receives a request containing an - // Expect field that includes an expectation- - // extension that it does not support, it MUST - // respond with a 417 (Expectation Failed) status." - w.Header().Set("Connection", "close") - w.WriteHeader(StatusExpectationFailed) - w.finishRequest() -} - -// Hijack implements the Hijacker.Hijack method. Our response is both a ResponseWriter -// and a Hijacker. -func (w *response) Hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error) { - if w.wroteHeader { - w.cw.flush() - } - // Release the bufioWriter that writes to the chunk writer, it is not - // used after a connection has been hijacked. - rwc, buf, err = w.conn.hijack() - if err == nil { - putBufioWriter(w.w) - w.w = nil - } - return rwc, buf, err -} - -func (w *response) CloseNotify() <-chan bool { - return w.conn.closeNotify() -} - -// The HandlerFunc type is an adapter to allow the use of -// ordinary functions as HTTP handlers. If f is a function -// with the appropriate signature, HandlerFunc(f) is a -// Handler object that calls f. -type HandlerFunc func(ResponseWriter, *Request) - -// ServeHTTP calls f(w, r). -func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { - f(w, r) -} - -// Helper handlers - -// Error replies to the request with the specified error message and HTTP code. -// The error message should be plain text. -func Error(w ResponseWriter, error string, code int) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(code) - fmt.Fprintln(w, error) -} - -// NotFound replies to the request with an HTTP 404 not found error. -func NotFound(w ResponseWriter, r *Request) { Error(w, "404 page not found", StatusNotFound) } - -// NotFoundHandler returns a simple request handler -// that replies to each request with a ``404 page not found'' reply. -func NotFoundHandler() Handler { return HandlerFunc(NotFound) } - -// StripPrefix returns a handler that serves HTTP requests -// by removing the given prefix from the request URL's Path -// and invoking the handler h. StripPrefix handles a -// request for a path that doesn't begin with prefix by -// replying with an HTTP 404 not found error. -func StripPrefix(prefix string, h Handler) Handler { - if prefix == "" { - return h - } - return HandlerFunc(func(w ResponseWriter, r *Request) { - if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) { - r.URL.Path = p - h.ServeHTTP(w, r) - } else { - NotFound(w, r) - } - }) -} - -// Redirect replies to the request with a redirect to url, -// which may be a path relative to the request path. -func Redirect(w ResponseWriter, r *Request, urlStr string, code int) { - if u, err := url.Parse(urlStr); err == nil { - // If url was relative, make absolute by - // combining with request path. - // The browser would probably do this for us, - // but doing it ourselves is more reliable. - - // NOTE(rsc): RFC 2616 says that the Location - // line must be an absolute URI, like - // "http://www.google.com/redirect/", - // not a path like "/redirect/". - // Unfortunately, we don't know what to - // put in the host name section to get the - // client to connect to us again, so we can't - // know the right absolute URI to send back. - // Because of this problem, no one pays attention - // to the RFC; they all send back just a new path. - // So do we. - oldpath := r.URL.Path - if oldpath == "" { // should not happen, but avoid a crash if it does - oldpath = "/" - } - if u.Scheme == "" { - // no leading http://server - if urlStr == "" || urlStr[0] != '/' { - // make relative path absolute - olddir, _ := path.Split(oldpath) - urlStr = olddir + urlStr - } - - var query string - if i := strings.Index(urlStr, "?"); i != -1 { - urlStr, query = urlStr[:i], urlStr[i:] - } - - // clean up but preserve trailing slash - trailing := strings.HasSuffix(urlStr, "/") - urlStr = path.Clean(urlStr) - if trailing && !strings.HasSuffix(urlStr, "/") { - urlStr += "/" - } - urlStr += query - } - } - - w.Header().Set("Location", urlStr) - w.WriteHeader(code) - - // RFC2616 recommends that a short note "SHOULD" be included in the - // response because older user agents may not understand 301/307. - // Shouldn't send the response for POST or HEAD; that leaves GET. - if r.Method == "GET" { - note := "" + statusText[code] + ".\n" - fmt.Fprintln(w, note) - } -} - -var htmlReplacer = strings.NewReplacer( - "&", "&", - "<", "<", - ">", ">", - // """ is shorter than """. - `"`, """, - // "'" is shorter than "'" and apos was not in HTML until HTML5. - "'", "'", -) - -func htmlEscape(s string) string { - return htmlReplacer.Replace(s) -} - -// Redirect to a fixed URL -type redirectHandler struct { - url string - code int -} - -func (rh *redirectHandler) ServeHTTP(w ResponseWriter, r *Request) { - Redirect(w, r, rh.url, rh.code) -} - -// RedirectHandler returns a request handler that redirects -// each request it receives to the given url using the given -// status code. -func RedirectHandler(url string, code int) Handler { - return &redirectHandler{url, code} -} - -// ServeMux is an HTTP request multiplexer. -// It matches the URL of each incoming request against a list of registered -// patterns and calls the handler for the pattern that -// most closely matches the URL. -// -// Patterns name fixed, rooted paths, like "/favicon.ico", -// or rooted subtrees, like "/images/" (note the trailing slash). -// Longer patterns take precedence over shorter ones, so that -// if there are handlers registered for both "/images/" -// and "/images/thumbnails/", the latter handler will be -// called for paths beginning "/images/thumbnails/" and the -// former will receive requests for any other paths in the -// "/images/" subtree. -// -// Note that since a pattern ending in a slash names a rooted subtree, -// the pattern "/" matches all paths not matched by other registered -// patterns, not just the URL with Path == "/". -// -// Patterns may optionally begin with a host name, restricting matches to -// URLs on that host only. Host-specific patterns take precedence over -// general patterns, so that a handler might register for the two patterns -// "/codesearch" and "codesearch.google.com/" without also taking over -// requests for "http://www.google.com/". -// -// ServeMux also takes care of sanitizing the URL request path, -// redirecting any request containing . or .. elements to an -// equivalent .- and ..-free URL. -type ServeMux struct { - mu sync.RWMutex - m map[string]muxEntry - hosts bool // whether any patterns contain hostnames -} - -type muxEntry struct { - explicit bool - h Handler - pattern string -} - -// NewServeMux allocates and returns a new ServeMux. -func NewServeMux() *ServeMux { return &ServeMux{m: make(map[string]muxEntry)} } - -// DefaultServeMux is the default ServeMux used by Serve. -var DefaultServeMux = NewServeMux() - -// Does path match pattern? -func pathMatch(pattern, path string) bool { - if len(pattern) == 0 { - // should not happen - return false - } - n := len(pattern) - if pattern[n-1] != '/' { - return pattern == path - } - return len(path) >= n && path[0:n] == pattern -} - -// Return the canonical path for p, eliminating . and .. elements. -func cleanPath(p string) string { - if p == "" { - return "/" - } - if p[0] != '/' { - p = "/" + p - } - np := path.Clean(p) - // path.Clean removes trailing slash except for root; - // put the trailing slash back if necessary. - if p[len(p)-1] == '/' && np != "/" { - np += "/" - } - return np -} - -// Find a handler on a handler map given a path string -// Most-specific (longest) pattern wins -func (mux *ServeMux) match(path string) (h Handler, pattern string) { - var n = 0 - for k, v := range mux.m { - if !pathMatch(k, path) { - continue - } - if h == nil || len(k) > n { - n = len(k) - h = v.h - pattern = v.pattern - } - } - return -} - -// Handler returns the handler to use for the given request, -// consulting r.Method, r.Host, and r.URL.Path. It always returns -// a non-nil handler. If the path is not in its canonical form, the -// handler will be an internally-generated handler that redirects -// to the canonical path. -// -// Handler also returns the registered pattern that matches the -// request or, in the case of internally-generated redirects, -// the pattern that will match after following the redirect. -// -// If there is no registered handler that applies to the request, -// Handler returns a ``page not found'' handler and an empty pattern. -func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { - if r.Method != "CONNECT" { - if p := cleanPath(r.URL.Path); p != r.URL.Path { - _, pattern = mux.handler(r.Host, p) - url := *r.URL - url.Path = p - return RedirectHandler(url.String(), StatusMovedPermanently), pattern - } - } - - return mux.handler(r.Host, r.URL.Path) -} - -// handler is the main implementation of Handler. -// The path is known to be in canonical form, except for CONNECT methods. -func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { - mux.mu.RLock() - defer mux.mu.RUnlock() - - // Host-specific pattern takes precedence over generic ones - if mux.hosts { - h, pattern = mux.match(host + path) - } - if h == nil { - h, pattern = mux.match(path) - } - if h == nil { - h, pattern = NotFoundHandler(), "" - } - return -} - -// ServeHTTP dispatches the request to the handler whose -// pattern most closely matches the request URL. -func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { - if r.RequestURI == "*" { - if r.ProtoAtLeast(1, 1) { - w.Header().Set("Connection", "close") - } - w.WriteHeader(StatusBadRequest) - return - } - h, _ := mux.Handler(r) - h.ServeHTTP(w, r) -} - -// Handle registers the handler for the given pattern. -// If a handler already exists for pattern, Handle panics. -func (mux *ServeMux) Handle(pattern string, handler Handler) { - mux.mu.Lock() - defer mux.mu.Unlock() - - if pattern == "" { - panic("http: invalid pattern " + pattern) - } - if handler == nil { - panic("http: nil handler") - } - if mux.m[pattern].explicit { - panic("http: multiple registrations for " + pattern) - } - - mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern} - - if pattern[0] != '/' { - mux.hosts = true - } - - // Helpful behavior: - // If pattern is /tree/, insert an implicit permanent redirect for /tree. - // It can be overridden by an explicit registration. - n := len(pattern) - if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit { - // If pattern contains a host name, strip it and use remaining - // path for redirect. - path := pattern - if pattern[0] != '/' { - // In pattern, at least the last character is a '/', so - // strings.Index can't be -1. - path = pattern[strings.Index(pattern, "/"):] - } - mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(path, StatusMovedPermanently), pattern: pattern} - } -} - -// HandleFunc registers the handler function for the given pattern. -func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { - mux.Handle(pattern, HandlerFunc(handler)) -} - -// Handle registers the handler for the given pattern -// in the DefaultServeMux. -// The documentation for ServeMux explains how patterns are matched. -func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) } - -// HandleFunc registers the handler function for the given pattern -// in the DefaultServeMux. -// The documentation for ServeMux explains how patterns are matched. -func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { - DefaultServeMux.HandleFunc(pattern, handler) -} - -// Serve accepts incoming HTTP connections on the listener l, -// creating a new service goroutine for each. The service goroutines -// read requests and then call handler to reply to them. -// Handler is typically nil, in which case the DefaultServeMux is used. -func Serve(l net.Listener, handler Handler) error { - srv := &Server{Handler: handler} - return srv.Serve(l) -} - -// A Server defines parameters for running an HTTP server. -// The zero value for Server is a valid configuration. -type Server struct { - Addr string // TCP address to listen on, ":http" if empty - Handler Handler // handler to invoke, http.DefaultServeMux if nil - ReadTimeout time.Duration // maximum duration before timing out read of the request - WriteTimeout time.Duration // maximum duration before timing out write of the response - MaxHeaderBytes int // maximum size of request headers, DefaultMaxHeaderBytes if 0 - TLSConfig *tls.Config // optional TLS config, used by ListenAndServeTLS - - // TLSNextProto optionally specifies a function to take over - // ownership of the provided TLS connection when an NPN - // protocol upgrade has occurred. The map key is the protocol - // name negotiated. The Handler argument should be used to - // handle HTTP requests and will initialize the Request's TLS - // and RemoteAddr if not already set. The connection is - // automatically closed when the function returns. - TLSNextProto map[string]func(*Server, *tls.Conn, Handler) - - // ConnState specifies an optional callback function that is - // called when a client connection changes state. See the - // ConnState type and associated constants for details. - ConnState func(net.Conn, ConnState) - - // ErrorLog specifies an optional logger for errors accepting - // connections and unexpected behavior from handlers. - // If nil, logging goes to os.Stderr via the log package's - // standard logger. - ErrorLog *log.Logger - - disableKeepAlives int32 // accessed atomically. -} - -// A ConnState represents the state of a client connection to a server. -// It's used by the optional Server.ConnState hook. -type ConnState int - -const ( - // StateNew represents a new connection that is expected to - // send a request immediately. Connections begin at this - // state and then transition to either StateActive or - // StateClosed. - StateNew ConnState = iota - - // StateActive represents a connection that has read 1 or more - // bytes of a request. The Server.ConnState hook for - // StateActive fires before the request has entered a handler - // and doesn't fire again until the request has been - // handled. After the request is handled, the state - // transitions to StateClosed, StateHijacked, or StateIdle. - StateActive - - // StateIdle represents a connection that has finished - // handling a request and is in the keep-alive state, waiting - // for a new request. Connections transition from StateIdle - // to either StateActive or StateClosed. - StateIdle - - // StateHijacked represents a hijacked connection. - // This is a terminal state. It does not transition to StateClosed. - StateHijacked - - // StateClosed represents a closed connection. - // This is a terminal state. Hijacked connections do not - // transition to StateClosed. - StateClosed -) - -var stateName = map[ConnState]string{ - StateNew: "new", - StateActive: "active", - StateIdle: "idle", - StateHijacked: "hijacked", - StateClosed: "closed", -} - -func (c ConnState) String() string { - return stateName[c] -} - -// serverHandler delegates to either the server's Handler or -// DefaultServeMux and also handles "OPTIONS *" requests. -type serverHandler struct { - srv *Server -} - -func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { - handler := sh.srv.Handler - if handler == nil { - handler = DefaultServeMux - } - if req.RequestURI == "*" && req.Method == "OPTIONS" { - handler = globalOptionsHandler{} - } - handler.ServeHTTP(rw, req) -} - -// ListenAndServe listens on the TCP network address srv.Addr and then -// calls Serve to handle requests on incoming connections. If -// srv.Addr is blank, ":http" is used. -func (srv *Server) ListenAndServe() error { - addr := srv.Addr - if addr == "" { - addr = ":http" - } - ln, err := net.Listen("tcp", addr) - if err != nil { - return err - } - return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) -} - -// Serve accepts incoming connections on the Listener l, creating a -// new service goroutine for each. The service goroutines read requests and -// then call srv.Handler to reply to them. -func (srv *Server) Serve(l net.Listener) error { - defer l.Close() - var tempDelay time.Duration // how long to sleep on accept failure - for { - rw, e := l.Accept() - if e != nil { - if ne, ok := e.(net.Error); ok && ne.Temporary() { - if tempDelay == 0 { - tempDelay = 5 * time.Millisecond - } else { - tempDelay *= 2 - } - if max := 1 * time.Second; tempDelay > max { - tempDelay = max - } - srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay) - time.Sleep(tempDelay) - continue - } - return e - } - tempDelay = 0 - c, err := srv.newConn(rw) - if err != nil { - continue - } - c.setState(c.rwc, StateNew) // before Serve can return - go c.serve() - } -} - -func (s *Server) doKeepAlives() bool { - return atomic.LoadInt32(&s.disableKeepAlives) == 0 -} - -// SetKeepAlivesEnabled controls whether HTTP keep-alives are enabled. -// By default, keep-alives are always enabled. Only very -// resource-constrained environments or servers in the process of -// shutting down should disable them. -func (s *Server) SetKeepAlivesEnabled(v bool) { - if v { - atomic.StoreInt32(&s.disableKeepAlives, 0) - } else { - atomic.StoreInt32(&s.disableKeepAlives, 1) - } -} - -func (s *Server) logf(format string, args ...interface{}) { - if s.ErrorLog != nil { - s.ErrorLog.Printf(format, args...) - } else { - log.Printf(format, args...) - } -} - -// ListenAndServe listens on the TCP network address addr -// and then calls Serve with handler to handle requests -// on incoming connections. Handler is typically nil, -// in which case the DefaultServeMux is used. -// -// A trivial example server is: -// -// package main -// -// import ( -// "io" -// "launchpad.net/ubuntu-push/http13client" -// "log" -// ) -// -// // hello world, the web server -// func HelloServer(w http.ResponseWriter, req *http.Request) { -// io.WriteString(w, "hello, world!\n") -// } -// -// func main() { -// http.HandleFunc("/hello", HelloServer) -// err := http.ListenAndServe(":12345", nil) -// if err != nil { -// log.Fatal("ListenAndServe: ", err) -// } -// } -func ListenAndServe(addr string, handler Handler) error { - server := &Server{Addr: addr, Handler: handler} - return server.ListenAndServe() -} - -// ListenAndServeTLS acts identically to ListenAndServe, except that it -// expects HTTPS connections. Additionally, files containing a certificate and -// matching private key for the server must be provided. If the certificate -// is signed by a certificate authority, the certFile should be the concatenation -// of the server's certificate followed by the CA's certificate. -// -// A trivial example server is: -// -// import ( -// "log" -// "launchpad.net/ubuntu-push/http13client" -// ) -// -// func handler(w http.ResponseWriter, req *http.Request) { -// w.Header().Set("Content-Type", "text/plain") -// w.Write([]byte("This is an example server.\n")) -// } -// -// func main() { -// http.HandleFunc("/", handler) -// log.Printf("About to listen on 10443. Go to https://127.0.0.1:10443/") -// err := http.ListenAndServeTLS(":10443", "cert.pem", "key.pem", nil) -// if err != nil { -// log.Fatal(err) -// } -// } -// -// One can use generate_cert.go in crypto/tls to generate cert.pem and key.pem. -func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler) error { - server := &Server{Addr: addr, Handler: handler} - return server.ListenAndServeTLS(certFile, keyFile) -} - -// ListenAndServeTLS listens on the TCP network address srv.Addr and -// then calls Serve to handle requests on incoming TLS connections. -// -// Filenames containing a certificate and matching private key for -// the server must be provided. If the certificate is signed by a -// certificate authority, the certFile should be the concatenation -// of the server's certificate followed by the CA's certificate. -// -// If srv.Addr is blank, ":https" is used. -func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error { - addr := srv.Addr - if addr == "" { - addr = ":https" - } - config := &tls.Config{} - if srv.TLSConfig != nil { - *config = *srv.TLSConfig - } - if config.NextProtos == nil { - config.NextProtos = []string{"http/1.1"} - } - - var err error - config.Certificates = make([]tls.Certificate, 1) - config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) - if err != nil { - return err - } - - ln, err := net.Listen("tcp", addr) - if err != nil { - return err - } - - tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config) - return srv.Serve(tlsListener) -} - -// TimeoutHandler returns a Handler that runs h with the given time limit. -// -// The new Handler calls h.ServeHTTP to handle each request, but if a -// call runs for longer than its time limit, the handler responds with -// a 503 Service Unavailable error and the given message in its body. -// (If msg is empty, a suitable default message will be sent.) -// After such a timeout, writes by h to its ResponseWriter will return -// ErrHandlerTimeout. -func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler { - f := func() <-chan time.Time { - return time.After(dt) - } - return &timeoutHandler{h, f, msg} -} - -// ErrHandlerTimeout is returned on ResponseWriter Write calls -// in handlers which have timed out. -var ErrHandlerTimeout = errors.New("http: Handler timeout") - -type timeoutHandler struct { - handler Handler - timeout func() <-chan time.Time // returns channel producing a timeout - body string -} - -func (h *timeoutHandler) errorBody() string { - if h.body != "" { - return h.body - } - return "Timeout

Timeout

" -} - -func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) { - done := make(chan bool, 1) - tw := &timeoutWriter{w: w} - go func() { - h.handler.ServeHTTP(tw, r) - done <- true - }() - select { - case <-done: - return - case <-h.timeout(): - tw.mu.Lock() - defer tw.mu.Unlock() - if !tw.wroteHeader { - tw.w.WriteHeader(StatusServiceUnavailable) - tw.w.Write([]byte(h.errorBody())) - } - tw.timedOut = true - } -} - -type timeoutWriter struct { - w ResponseWriter - - mu sync.Mutex - timedOut bool - wroteHeader bool -} - -func (tw *timeoutWriter) Header() Header { - return tw.w.Header() -} - -func (tw *timeoutWriter) Write(p []byte) (int, error) { - tw.mu.Lock() - timedOut := tw.timedOut - tw.mu.Unlock() - if timedOut { - return 0, ErrHandlerTimeout - } - return tw.w.Write(p) -} - -func (tw *timeoutWriter) WriteHeader(code int) { - tw.mu.Lock() - if tw.timedOut || tw.wroteHeader { - tw.mu.Unlock() - return - } - tw.wroteHeader = true - tw.mu.Unlock() - tw.w.WriteHeader(code) -} - -// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted -// connections. It's used by ListenAndServe and ListenAndServeTLS so -// dead TCP connections (e.g. closing laptop mid-download) eventually -// go away. -type tcpKeepAliveListener struct { - *net.TCPListener -} - -func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { - tc, err := ln.AcceptTCP() - if err != nil { - return - } - tc.SetKeepAlive(true) - tc.SetKeepAlivePeriod(3 * time.Minute) - return tc, nil -} - -// globalOptionsHandler responds to "OPTIONS *" requests. -type globalOptionsHandler struct{} - -func (globalOptionsHandler) ServeHTTP(w ResponseWriter, r *Request) { - w.Header().Set("Content-Length", "0") - if r.ContentLength != 0 { - // Read up to 4KB of OPTIONS body (as mentioned in the - // spec as being reserved for future use), but anything - // over that is considered a waste of server resources - // (or an attack) and we abort and close the connection, - // courtesy of MaxBytesReader's EOF behavior. - mb := MaxBytesReader(w, r.Body, 4<<10) - io.Copy(ioutil.Discard, mb) - } -} +) type eofReaderWithWriteTo struct{} @@ -1991,28 +31,6 @@ // Verify that an io.Copy from an eofReader won't require a buffer. var _ io.WriterTo = eofReader -// initNPNRequest is an HTTP handler that initializes certain -// uninitialized fields in its *Request. Such partially-initialized -// Requests come from NPN protocol handlers. -type initNPNRequest struct { - c *tls.Conn - h serverHandler -} - -func (h initNPNRequest) ServeHTTP(rw ResponseWriter, req *Request) { - if req.TLS == nil { - req.TLS = &tls.ConnectionState{} - *req.TLS = h.c.ConnectionState() - } - if req.Body == nil { - req.Body = eofReader - } - if req.RemoteAddr == "" { - req.RemoteAddr = h.c.RemoteAddr().String() - } - h.h.ServeHTTP(rw, req) -} - // loggingConn is used for debugging. type loggingConn struct { name string ubuntu-push-0.68+16.04.20160310.2/http13client/_patches/fix_status.patch0000644000015600001650000001006612670364255026002 0ustar pbuserpbgroup00000000000000=== modified file 'http13client/client.go' --- http13client/client.go 2014-06-20 12:46:25 +0000 +++ http13client/client.go 2014-06-20 12:46:45 +0000 @@ -217,7 +217,7 @@ // automatically redirect. func shouldRedirectGet(statusCode int) bool { switch statusCode { - case StatusMovedPermanently, StatusFound, StatusSeeOther, StatusTemporaryRedirect: + case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect: return true } return false @@ -227,7 +227,7 @@ // automatically redirect. func shouldRedirectPost(statusCode int) bool { switch statusCode { - case StatusFound, StatusSeeOther: + case http.StatusFound, http.StatusSeeOther: return true } return false === modified file 'http13client/client_test.go' --- http13client/client_test.go 2014-06-20 12:46:25 +0000 +++ http13client/client_test.go 2014-06-20 12:46:45 +0000 @@ -204,7 +204,7 @@ } } if n < 15 { - http.Redirect(w, r, fmt.Sprintf("/?n=%d", n+1), StatusFound) + http.Redirect(w, r, fmt.Sprintf("/?n=%d", n+1), http.StatusFound) return } fmt.Fprintf(w, "n=%d", n) @@ -326,7 +326,7 @@ } if r.URL.Path == "/" { http.SetCookie(w, expectedCookies[1]) - http.Redirect(w, r, "/second", StatusMovedPermanently) + http.Redirect(w, r, "/second", http.StatusMovedPermanently) } else { http.SetCookie(w, expectedCookies[2]) w.Write([]byte("hello")) @@ -785,7 +785,7 @@ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { sawRoot <- true - http.Redirect(w, r, "/slow", StatusFound) + http.Redirect(w, r, "/slow", http.StatusFound) return } if r.URL.Path == "/slow" { @@ -846,7 +846,7 @@ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { saw <- r.RemoteAddr if r.URL.Path == "/" { - http.Redirect(w, r, "/foo", StatusFound) // which includes a body + http.Redirect(w, r, "/foo", http.StatusFound) // which includes a body } })) defer ts.Close() === modified file 'http13client/request_test.go' --- http13client/request_test.go 2014-06-20 12:46:25 +0000 +++ http13client/request_test.go 2014-06-20 12:46:45 +0000 @@ -182,11 +182,11 @@ switch r.URL.Path { case "/": w.Header().Set("Location", "/foo/") - w.WriteHeader(StatusSeeOther) + w.WriteHeader(http.StatusSeeOther) case "/foo/": fmt.Fprintf(w, "foo") default: - w.WriteHeader(StatusBadRequest) + w.WriteHeader(http.StatusBadRequest) } })) defer ts.Close() === modified file 'http13client/response.go' --- http13client/response.go 2014-06-20 12:46:25 +0000 +++ http13client/response.go 2014-06-20 12:46:45 +0000 @@ -205,9 +205,8 @@ // Status line text := r.Status if text == "" { - var ok bool - text, ok = statusText[r.StatusCode] - if !ok { + text = http.StatusText(r.StatusCode) + if text == "" { text = "status code " + strconv.Itoa(r.StatusCode) } } === modified file 'http13client/responsewrite_test.go' --- http13client/responsewrite_test.go 2014-06-20 12:46:25 +0000 +++ http13client/responsewrite_test.go 2014-06-20 12:47:05 +0000 @@ -197,7 +197,7 @@ // there were two. { Response{ - StatusCode: StatusOK, + StatusCode: http.StatusOK, ProtoMajor: 1, ProtoMinor: 1, Request: &Request{Method: "POST"}, === modified file 'http13client/transport_test.go' --- http13client/transport_test.go 2014-06-20 12:46:25 +0000 +++ http13client/transport_test.go 2014-06-20 12:46:45 +0000 @@ -1004,7 +1004,7 @@ defer afterTest(t) const deniedMsg = "sorry, denied." ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, deniedMsg, StatusUnauthorized) + http.Error(w, deniedMsg, http.StatusUnauthorized) })) defer ts.Close() tr := &Transport{} @@ -1028,7 +1028,7 @@ func TestChunkedNoContent(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(StatusNoContent) + w.WriteHeader(http.StatusNoContent) })) defer ts.Close() ubuntu-push-0.68+16.04.20160310.2/http13client/_patches/sync_pool.Rpatch0000644000015600001650000001010112670364255025726 0ustar pbuserpbgroup00000000000000diff -r ff4cb9143edb -r 3d2ece96ab95 src/pkg/net/http/header.go --- a/src/pkg/net/http/header.go Wed Dec 18 15:52:05 2013 -0800 +++ b/src/pkg/net/http/header.go Wed Dec 18 15:52:20 2013 -0800 @@ -9,6 +9,7 @@ "net/textproto" "sort" "strings" + "sync" "time" ) @@ -114,18 +115,15 @@ func (s *headerSorter) Swap(i, j int) { s.kvs[i], s.kvs[j] = s.kvs[j], s.kvs[i] } func (s *headerSorter) Less(i, j int) bool { return s.kvs[i].key < s.kvs[j].key } -// TODO: convert this to a sync.Cache (issue 4720) -var headerSorterCache = make(chan *headerSorter, 8) +var headerSorterPool = sync.Pool{ + New: func() interface{} { return new(headerSorter) }, +} // sortedKeyValues returns h's keys sorted in the returned kvs // slice. The headerSorter used to sort is also returned, for possible // return to headerSorterCache. func (h Header) sortedKeyValues(exclude map[string]bool) (kvs []keyValues, hs *headerSorter) { - select { - case hs = <-headerSorterCache: - default: - hs = new(headerSorter) - } + hs = headerSorterPool.Get().(*headerSorter) if cap(hs.kvs) < len(h) { hs.kvs = make([]keyValues, 0, len(h)) } @@ -159,10 +157,7 @@ } } } - select { - case headerSorterCache <- sorter: - default: - } + headerSorterPool.Put(sorter) return nil } diff -r ff4cb9143edb -r 3d2ece96ab95 src/pkg/net/http/request.go --- a/src/pkg/net/http/request.go Wed Dec 18 15:52:05 2013 -0800 +++ b/src/pkg/net/http/request.go Wed Dec 18 15:52:20 2013 -0800 @@ -20,6 +20,7 @@ "net/url" "strconv" "strings" + "sync" ) const ( @@ -494,25 +495,20 @@ return line[:s1], line[s1+1 : s2], line[s2+1:], true } -// TODO(bradfitz): use a sync.Cache when available -var textprotoReaderCache = make(chan *textproto.Reader, 4) +var textprotoReaderPool sync.Pool func newTextprotoReader(br *bufio.Reader) *textproto.Reader { - select { - case r := <-textprotoReaderCache: - r.R = br - return r - default: - return textproto.NewReader(br) + if v := textprotoReaderPool.Get(); v != nil { + tr := v.(*textproto.Reader) + tr.R = br + return tr } + return textproto.NewReader(br) } func putTextprotoReader(r *textproto.Reader) { r.R = nil - select { - case textprotoReaderCache <- r: - default: - } + textprotoReaderPool.Put(r) } // ReadRequest reads and parses a request from b. diff -r ff4cb9143edb -r 3d2ece96ab95 src/pkg/net/http/server.go --- a/src/pkg/net/http/server.go Wed Dec 18 15:52:05 2013 -0800 +++ b/src/pkg/net/http/server.go Wed Dec 18 15:52:20 2013 -0800 @@ -435,56 +435,52 @@ return c, nil } -// TODO: use a sync.Cache instead var ( - bufioReaderCache = make(chan *bufio.Reader, 4) - bufioWriterCache2k = make(chan *bufio.Writer, 4) - bufioWriterCache4k = make(chan *bufio.Writer, 4) + bufioReaderPool sync.Pool + bufioWriter2kPool sync.Pool + bufioWriter4kPool sync.Pool ) -func bufioWriterCache(size int) chan *bufio.Writer { +func bufioWriterPool(size int) *sync.Pool { switch size { case 2 << 10: - return bufioWriterCache2k + return &bufioWriter2kPool case 4 << 10: - return bufioWriterCache4k + return &bufioWriter4kPool } return nil } func newBufioReader(r io.Reader) *bufio.Reader { - select { - case p := <-bufioReaderCache: - p.Reset(r) - return p - default: - return bufio.NewReader(r) + if v := bufioReaderPool.Get(); v != nil { + br := v.(*bufio.Reader) + br.Reset(r) + return br } + return bufio.NewReader(r) } func putBufioReader(br *bufio.Reader) { br.Reset(nil) - select { - case bufioReaderCache <- br: - default: - } + bufioReaderPool.Put(br) } func newBufioWriterSize(w io.Writer, size int) *bufio.Writer { - select { - case p := <-bufioWriterCache(size): - p.Reset(w) - return p - default: - return bufio.NewWriterSize(w, size) + pool := bufioWriterPool(size) + if pool != nil { + if v := pool.Get(); v != nil { + bw := v.(*bufio.Writer) + bw.Reset(w) + return bw + } } + return bufio.NewWriterSize(w, size) } func putBufioWriter(bw *bufio.Writer) { bw.Reset(nil) - select { - case bufioWriterCache(bw.Available()) <- bw: - default: + if pool := bufioWriterPool(bw.Available()); pool != nil { + pool.Put(bw) } } ubuntu-push-0.68+16.04.20160310.2/http13client/_patches/no_keepalive.patch0000644000015600001650000000051712670364255026252 0ustar pbuserpbgroup00000000000000--- a/transport.go 2014-03-19 19:53:49.401406590 +0000 +++ b/transport.go 2014-03-19 19:54:05.817611304 +0000 @@ -34,7 +34,7 @@ Proxy: ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, + // KeepAlive: 30 * time.Second, }).Dial, TLSHandshakeTimeout: 10 * time.Second, } ubuntu-push-0.68+16.04.20160310.2/http13client/_patches/fix_code.patch0000644000015600001650000005144512670364255025377 0ustar pbuserpbgroup00000000000000=== modified file 'http13client/client.go' --- http13client/client.go 2014-06-20 11:00:47 +0000 +++ http13client/client.go 2014-06-20 12:05:53 +0000 @@ -17,6 +17,7 @@ "io/ioutil" "log" "net/url" + "net/http" "strings" "sync" "time" @@ -54,7 +55,7 @@ // Jar specifies the cookie jar. // If Jar is nil, cookies are not sent in requests and ignored // in responses. - Jar CookieJar + Jar http.CookieJar // Timeout specifies a time limit for requests made by this // Client. The timeout includes connection time, any @@ -184,7 +185,7 @@ // Headers, leaving it uninitialized. We guarantee to the // Transport that this has been initialized, though. if req.Header == nil { - req.Header = make(Header) + req.Header = make(http.Header) } if u := req.URL.User; u != nil { @@ -316,7 +317,7 @@ if ireq.Method == "POST" || ireq.Method == "PUT" { nreq.Method = "GET" } - nreq.Header = make(Header) + nreq.Header = make(http.Header) nreq.URL, err = base.Parse(urlStr) if err != nil { break === modified file 'http13client/cookie.go' --- http13client/cookie.go 2014-06-20 11:00:47 +0000 +++ http13client/cookie.go 2014-06-20 12:05:53 +0000 @@ -5,10 +5,9 @@ package http import ( - "bytes" - "fmt" "log" "net" + "net/http" "strconv" "strings" "time" @@ -18,30 +17,10 @@ // // http://tools.ietf.org/html/rfc6265 -// A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an -// HTTP response or the Cookie header of an HTTP request. -type Cookie struct { - Name string - Value string - Path string - Domain string - Expires time.Time - RawExpires string - - // MaxAge=0 means no 'Max-Age' attribute specified. - // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' - // MaxAge>0 means Max-Age attribute present and given in seconds - MaxAge int - Secure bool - HttpOnly bool - Raw string - Unparsed []string // Raw text of unparsed attribute-value pairs -} - // readSetCookies parses all "Set-Cookie" values from // the header h and returns the successfully parsed Cookies. -func readSetCookies(h Header) []*Cookie { - cookies := []*Cookie{} +func readSetCookies(h http.Header) []*http.Cookie { + cookies := []*http.Cookie{} for _, line := range h["Set-Cookie"] { parts := strings.Split(strings.TrimSpace(line), ";") if len(parts) == 1 && parts[0] == "" { @@ -60,7 +39,7 @@ if !success { continue } - c := &Cookie{ + c := &http.Cookie{ Name: name, Value: value, Raw: line, @@ -125,59 +104,12 @@ return cookies } -// SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers. -func SetCookie(w ResponseWriter, cookie *Cookie) { - w.Header().Add("Set-Cookie", cookie.String()) -} - -// String returns the serialization of the cookie for use in a Cookie -// header (if only Name and Value are set) or a Set-Cookie response -// header (if other fields are set). -func (c *Cookie) String() string { - var b bytes.Buffer - fmt.Fprintf(&b, "%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value)) - if len(c.Path) > 0 { - fmt.Fprintf(&b, "; Path=%s", sanitizeCookiePath(c.Path)) - } - if len(c.Domain) > 0 { - if validCookieDomain(c.Domain) { - // A c.Domain containing illegal characters is not - // sanitized but simply dropped which turns the cookie - // into a host-only cookie. A leading dot is okay - // but won't be sent. - d := c.Domain - if d[0] == '.' { - d = d[1:] - } - fmt.Fprintf(&b, "; Domain=%s", d) - } else { - log.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute", - c.Domain) - } - } - if c.Expires.Unix() > 0 { - fmt.Fprintf(&b, "; Expires=%s", c.Expires.UTC().Format(time.RFC1123)) - } - if c.MaxAge > 0 { - fmt.Fprintf(&b, "; Max-Age=%d", c.MaxAge) - } else if c.MaxAge < 0 { - fmt.Fprintf(&b, "; Max-Age=0") - } - if c.HttpOnly { - fmt.Fprintf(&b, "; HttpOnly") - } - if c.Secure { - fmt.Fprintf(&b, "; Secure") - } - return b.String() -} - // readCookies parses all "Cookie" values from the header h and // returns the successfully parsed Cookies. // // if filter isn't empty, only cookies of that name are returned -func readCookies(h Header, filter string) []*Cookie { - cookies := []*Cookie{} +func readCookies(h http.Header, filter string) []*http.Cookie { + cookies := []*http.Cookie{} lines, ok := h["Cookie"] if !ok { return cookies @@ -209,7 +141,7 @@ if !success { continue } - cookies = append(cookies, &Cookie{Name: name, Value: val}) + cookies = append(cookies, &http.Cookie{Name: name, Value: val}) parsedPairs++ } } === modified file 'http13client/header.go' --- http13client/header.go 2014-06-20 11:00:47 +0000 +++ http13client/header.go 2014-06-20 12:00:22 +0000 @@ -5,176 +5,9 @@ package http import ( - "io" - "net/textproto" - "sort" "strings" - "time" ) -var raceEnabled = false // set by race.go - -// A Header represents the key-value pairs in an HTTP header. -type Header map[string][]string - -// Add adds the key, value pair to the header. -// It appends to any existing values associated with key. -func (h Header) Add(key, value string) { - textproto.MIMEHeader(h).Add(key, value) -} - -// Set sets the header entries associated with key to -// the single element value. It replaces any existing -// values associated with key. -func (h Header) Set(key, value string) { - textproto.MIMEHeader(h).Set(key, value) -} - -// Get gets the first value associated with the given key. -// If there are no values associated with the key, Get returns "". -// To access multiple values of a key, access the map directly -// with CanonicalHeaderKey. -func (h Header) Get(key string) string { - return textproto.MIMEHeader(h).Get(key) -} - -// get is like Get, but key must already be in CanonicalHeaderKey form. -func (h Header) get(key string) string { - if v := h[key]; len(v) > 0 { - return v[0] - } - return "" -} - -// Del deletes the values associated with key. -func (h Header) Del(key string) { - textproto.MIMEHeader(h).Del(key) -} - -// Write writes a header in wire format. -func (h Header) Write(w io.Writer) error { - return h.WriteSubset(w, nil) -} - -func (h Header) clone() Header { - h2 := make(Header, len(h)) - for k, vv := range h { - vv2 := make([]string, len(vv)) - copy(vv2, vv) - h2[k] = vv2 - } - return h2 -} - -var timeFormats = []string{ - TimeFormat, - time.RFC850, - time.ANSIC, -} - -// ParseTime parses a time header (such as the Date: header), -// trying each of the three formats allowed by HTTP/1.1: -// TimeFormat, time.RFC850, and time.ANSIC. -func ParseTime(text string) (t time.Time, err error) { - for _, layout := range timeFormats { - t, err = time.Parse(layout, text) - if err == nil { - return - } - } - return -} - -var headerNewlineToSpace = strings.NewReplacer("\n", " ", "\r", " ") - -type writeStringer interface { - WriteString(string) (int, error) -} - -// stringWriter implements WriteString on a Writer. -type stringWriter struct { - w io.Writer -} - -func (w stringWriter) WriteString(s string) (n int, err error) { - return w.w.Write([]byte(s)) -} - -type keyValues struct { - key string - values []string -} - -// A headerSorter implements sort.Interface by sorting a []keyValues -// by key. It's used as a pointer, so it can fit in a sort.Interface -// interface value without allocation. -type headerSorter struct { - kvs []keyValues -} - -func (s *headerSorter) Len() int { return len(s.kvs) } -func (s *headerSorter) Swap(i, j int) { s.kvs[i], s.kvs[j] = s.kvs[j], s.kvs[i] } -func (s *headerSorter) Less(i, j int) bool { return s.kvs[i].key < s.kvs[j].key } - -// TODO: convert this to a sync.Cache (issue 4720) -var headerSorterCache = make(chan *headerSorter, 8) - -// sortedKeyValues returns h's keys sorted in the returned kvs -// slice. The headerSorter used to sort is also returned, for possible -// return to headerSorterCache. -func (h Header) sortedKeyValues(exclude map[string]bool) (kvs []keyValues, hs *headerSorter) { - select { - case hs = <-headerSorterCache: - default: - hs = new(headerSorter) - } - if cap(hs.kvs) < len(h) { - hs.kvs = make([]keyValues, 0, len(h)) - } - kvs = hs.kvs[:0] - for k, vv := range h { - if !exclude[k] { - kvs = append(kvs, keyValues{k, vv}) - } - } - hs.kvs = kvs - sort.Sort(hs) - return kvs, hs -} - -// WriteSubset writes a header in wire format. -// If exclude is not nil, keys where exclude[key] == true are not written. -func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error { - ws, ok := w.(writeStringer) - if !ok { - ws = stringWriter{w} - } - kvs, sorter := h.sortedKeyValues(exclude) - for _, kv := range kvs { - for _, v := range kv.values { - v = headerNewlineToSpace.Replace(v) - v = textproto.TrimString(v) - for _, s := range []string{kv.key, ": ", v, "\r\n"} { - if _, err := ws.WriteString(s); err != nil { - return err - } - } - } - } - select { - case headerSorterCache <- sorter: - default: - } - return nil -} - -// CanonicalHeaderKey returns the canonical format of the -// header key s. The canonicalization converts the first -// letter and any letter following a hyphen to upper case; -// the rest are converted to lowercase. For example, the -// canonical key for "accept-encoding" is "Accept-Encoding". -func CanonicalHeaderKey(s string) string { return textproto.CanonicalMIMEHeaderKey(s) } - // hasToken reports whether token appears with v, ASCII // case-insensitive, with space or comma boundaries. // token must be all lowercase. === modified file 'http13client/request.go' --- http13client/request.go 2014-06-20 11:00:47 +0000 +++ http13client/request.go 2014-06-20 12:05:53 +0000 @@ -16,6 +16,7 @@ "io/ioutil" "mime" "mime/multipart" + "net/http" "net/textproto" "net/url" "strconv" @@ -121,7 +122,7 @@ // added and may override values in Header. // // See the documentation for the Request.Write method. - Header Header + Header http.Header // Body is the request's body. // @@ -199,7 +200,7 @@ // not mutate Trailer. // // Few HTTP clients, servers, or proxies support HTTP trailers. - Trailer Header + Trailer http.Header // RemoteAddr allows HTTP servers and other software to record // the network address that sent the request, usually for @@ -239,7 +240,7 @@ } // Cookies parses and returns the HTTP cookies sent with the request. -func (r *Request) Cookies() []*Cookie { +func (r *Request) Cookies() []*http.Cookie { return readCookies(r.Header, "") } @@ -247,7 +248,7 @@ // Cookie returns the named cookie provided in the request or // ErrNoCookie if not found. -func (r *Request) Cookie(name string) (*Cookie, error) { +func (r *Request) Cookie(name string) (*http.Cookie, error) { for _, c := range readCookies(r.Header, name) { return c, nil } @@ -258,7 +259,7 @@ // AddCookie does not attach more than one Cookie header field. That // means all cookies, if any, are written into the same line, // separated by semicolon. -func (r *Request) AddCookie(c *Cookie) { +func (r *Request) AddCookie(c *http.Cookie) { s := fmt.Sprintf("%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value)) if c := r.Header.Get("Cookie"); c != "" { r.Header.Set("Cookie", c+"; "+s) @@ -361,7 +362,7 @@ } // extraHeaders may be nil -func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header) error { +func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders http.Header) error { host := req.Host if host == "" { if req.URL == nil { @@ -490,7 +491,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: make(Header), + Header: make(http.Header), Body: rc, Host: u.Host, } @@ -605,7 +606,7 @@ if err != nil { return nil, err } - req.Header = Header(mimeHeader) + req.Header = http.Header(mimeHeader) // RFC2616: Must treat // GET /index.html HTTP/1.1 @@ -616,7 +617,7 @@ // the same. In the second case, any Host line is ignored. req.Host = req.URL.Host if req.Host == "" { - req.Host = req.Header.get("Host") + req.Host = req.Header.Get("Host") } delete(req.Header, "Host") @@ -638,12 +639,12 @@ // // MaxBytesReader prevents clients from accidentally or maliciously // sending a large request and wasting server resources. -func MaxBytesReader(w ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser { +func MaxBytesReader(w http.ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser { return &maxBytesReader{w: w, r: r, n: n} } type maxBytesReader struct { - w ResponseWriter + w http.ResponseWriter r io.ReadCloser // underlying reader n int64 // max bytes remaining stopped bool @@ -653,9 +654,6 @@ if l.n <= 0 { if !l.stopped { l.stopped = true - if res, ok := l.w.(*response); ok { - res.requestTooLarge() - } } return 0, errors.New("http: request body too large") } @@ -858,18 +856,18 @@ } func (r *Request) expectsContinue() bool { - return hasToken(r.Header.get("Expect"), "100-continue") + return hasToken(r.Header.Get("Expect"), "100-continue") } func (r *Request) wantsHttp10KeepAlive() bool { if r.ProtoMajor != 1 || r.ProtoMinor != 0 { return false } - return hasToken(r.Header.get("Connection"), "keep-alive") + return hasToken(r.Header.Get("Connection"), "keep-alive") } func (r *Request) wantsClose() bool { - return hasToken(r.Header.get("Connection"), "close") + return hasToken(r.Header.Get("Connection"), "close") } func (r *Request) closeBody() { === modified file 'http13client/response.go' --- http13client/response.go 2014-06-20 11:00:47 +0000 +++ http13client/response.go 2014-06-20 12:05:53 +0000 @@ -12,6 +12,7 @@ "crypto/tls" "errors" "io" + "net/http" "net/textproto" "net/url" "strconv" @@ -41,7 +42,7 @@ // omitted from Header. // // Keys in the map are canonicalized (see CanonicalHeaderKey). - Header Header + Header http.Header // Body represents the response body. // @@ -71,7 +72,7 @@ // Trailer maps trailer keys to values, in the same // format as the header. - Trailer Header + Trailer http.Header // The Request that was sent to obtain this Response. // Request's Body is nil (having already been consumed). @@ -86,7 +87,7 @@ } // Cookies parses and returns the cookies set in the Set-Cookie headers. -func (r *Response) Cookies() []*Cookie { +func (r *Response) Cookies() []*http.Cookie { return readSetCookies(r.Header) } @@ -155,7 +156,7 @@ } return nil, err } - resp.Header = Header(mimeHeader) + resp.Header = http.Header(mimeHeader) fixPragmaCacheControl(resp.Header) @@ -171,7 +172,7 @@ // Pragma: no-cache // like // Cache-Control: no-cache -func fixPragmaCacheControl(header Header) { +func fixPragmaCacheControl(header http.Header) { if hp, ok := header["Pragma"]; ok && len(hp) > 0 && hp[0] == "no-cache" { if _, presentcc := header["Cache-Control"]; !presentcc { header["Cache-Control"] = []string{"no-cache"} === modified file 'http13client/transfer.go' --- http13client/transfer.go 2014-06-20 11:00:47 +0000 +++ http13client/transfer.go 2014-06-20 12:05:53 +0000 @@ -11,6 +11,7 @@ "fmt" "io" "io/ioutil" + "net/http" "net/textproto" "sort" "strconv" @@ -37,7 +38,7 @@ ContentLength int64 // -1 means unknown, 0 means exactly none Close bool TransferEncoding []string - Trailer Header + Trailer http.Header } func newTransferWriter(r interface{}) (t *transferWriter, err error) { @@ -171,7 +172,7 @@ if t.Trailer != nil { keys := make([]string, 0, len(t.Trailer)) for k := range t.Trailer { - k = CanonicalHeaderKey(k) + k = http.CanonicalHeaderKey(k) switch k { case "Transfer-Encoding", "Trailer", "Content-Length": return &badStringError{"invalid Trailer key", k} @@ -243,7 +244,7 @@ type transferReader struct { // Input - Header Header + Header http.Header StatusCode int RequestMethod string ProtoMajor int @@ -253,7 +254,7 @@ ContentLength int64 TransferEncoding []string Close bool - Trailer Header + Trailer http.Header } // bodyAllowedForStatus reports whether a given response status code @@ -330,7 +331,7 @@ return err } if isResponse && t.RequestMethod == "HEAD" { - if n, err := parseContentLength(t.Header.get("Content-Length")); err != nil { + if n, err := parseContentLength(t.Header.Get("Content-Length")); err != nil { return err } else { t.ContentLength = n @@ -408,7 +409,7 @@ func isIdentity(te []string) bool { return len(te) == 1 && te[0] == "identity" } // Sanitize transfer encoding -func fixTransferEncoding(requestMethod string, header Header) ([]string, error) { +func fixTransferEncoding(requestMethod string, header http.Header) ([]string, error) { raw, present := header["Transfer-Encoding"] if !present { return nil, nil @@ -451,7 +452,7 @@ // Determine the expected body length, using RFC 2616 Section 4.4. This // function is not a method, because ultimately it should be shared by // ReadResponse and ReadRequest. -func fixLength(isResponse bool, status int, requestMethod string, header Header, te []string) (int64, error) { +func fixLength(isResponse bool, status int, requestMethod string, header http.Header, te []string) (int64, error) { // Logic based on response type or status if noBodyExpected(requestMethod) { @@ -471,7 +472,7 @@ } // Logic based on Content-Length - cl := strings.TrimSpace(header.get("Content-Length")) + cl := strings.TrimSpace(header.Get("Content-Length")) if cl != "" { n, err := parseContentLength(cl) if err != nil { @@ -497,18 +498,18 @@ // Determine whether to hang up after sending a request and body, or // receiving a response and body // 'header' is the request headers -func shouldClose(major, minor int, header Header) bool { +func shouldClose(major, minor int, header http.Header) bool { if major < 1 { return true } else if major == 1 && minor == 0 { - if !strings.Contains(strings.ToLower(header.get("Connection")), "keep-alive") { + if !strings.Contains(strings.ToLower(header.Get("Connection")), "keep-alive") { return true } return false } else { // TODO: Should split on commas, toss surrounding white space, // and check each field. - if strings.ToLower(header.get("Connection")) == "close" { + if strings.ToLower(header.Get("Connection")) == "close" { header.Del("Connection") return true } @@ -517,17 +518,17 @@ } // Parse the trailer header -func fixTrailer(header Header, te []string) (Header, error) { - raw := header.get("Trailer") +func fixTrailer(header http.Header, te []string) (http.Header, error) { + raw := header.Get("Trailer") if raw == "" { return nil, nil } header.Del("Trailer") - trailer := make(Header) + trailer := make(http.Header) keys := strings.Split(raw, ",") for _, key := range keys { - key = CanonicalHeaderKey(strings.TrimSpace(key)) + key = http.CanonicalHeaderKey(strings.TrimSpace(key)) switch key { case "Transfer-Encoding", "Trailer", "Content-Length": return nil, &badStringError{"bad trailer key", key} @@ -664,14 +665,14 @@ } switch rr := b.hdr.(type) { case *Request: - mergeSetHeader(&rr.Trailer, Header(hdr)) + mergeSetHeader(&rr.Trailer, http.Header(hdr)) case *Response: - mergeSetHeader(&rr.Trailer, Header(hdr)) + mergeSetHeader(&rr.Trailer, http.Header(hdr)) } return nil } -func mergeSetHeader(dst *Header, src Header) { +func mergeSetHeader(dst *http.Header, src http.Header) { if *dst == nil { *dst = src return === modified file 'http13client/transport.go' --- http13client/transport.go 2014-06-20 11:00:47 +0000 +++ http13client/transport.go 2014-06-20 12:05:53 +0000 @@ -18,6 +18,7 @@ "io" "log" "net" + "net/http" "net/url" "os" "strings" @@ -147,12 +148,12 @@ // optional extra headers to write. type transportRequest struct { *Request // original request, not to be mutated - extra Header // extra headers to write, or nil + extra http.Header // extra headers to write, or nil } -func (tr *transportRequest) extraHeaders() Header { +func (tr *transportRequest) extraHeaders() http.Header { if tr.extra == nil { - tr.extra = make(Header) + tr.extra = make(http.Header) } return tr.extra } @@ -519,7 +520,7 @@ case cm.targetScheme == "http": pconn.isProxy = true if pa != "" { - pconn.mutateHeaderFunc = func(h Header) { + pconn.mutateHeaderFunc = func(h http.Header) { h.Set("Proxy-Authorization", pa) } } @@ -528,7 +529,7 @@ Method: "CONNECT", URL: &url.URL{Opaque: cm.targetAddr}, Host: cm.targetAddr, - Header: make(Header), + Header: make(http.Header), } if pa != "" { connectReq.Header.Set("Proxy-Authorization", pa) @@ -748,7 +749,7 @@ // mutateHeaderFunc is an optional func to modify extra // headers on each outbound request before it's written. (the // original Request given to RoundTrip is not modified) - mutateHeaderFunc func(Header) + mutateHeaderFunc func(http.Header) } // isBroken reports whether this connection is in a known broken state. ubuntu-push-0.68+16.04.20160310.2/http13client/_patches/no_serve_test_unsupported_bench.patch0000644000015600001650000000216612670364255032301 0ustar pbuserpbgroup00000000000000--- a/serve_test.go 2014-03-19 20:14:01.831371373 +0000 +++ b/serve_test.go 2014-03-19 20:14:47.518205363 +0000 @@ -2474,43 +2474,6 @@ b.StopTimer() } -func BenchmarkClientServerParallel4(b *testing.B) { - benchmarkClientServerParallel(b, 4) -} - -func BenchmarkClientServerParallel64(b *testing.B) { - benchmarkClientServerParallel(b, 64) -} - -func benchmarkClientServerParallel(b *testing.B, parallelism int) { - b.ReportAllocs() - ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) { - fmt.Fprintf(rw, "Hello world.\n") - })) - defer ts.Close() - b.ResetTimer() - b.SetParallelism(parallelism) - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - res, err := Get(ts.URL) - if err != nil { - b.Logf("Get: %v", err) - continue - } - all, err := ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - b.Logf("ReadAll: %v", err) - continue - } - body := string(all) - if body != "Hello world.\n" { - panic("Got body: " + body) - } - } - }) -} - // A benchmark for profiling the server without the HTTP client code. // The client code runs in a subprocess. // ubuntu-push-0.68+16.04.20160310.2/http13client/_patches/fix_tests.patch0000644000015600001650000013452712670364255025632 0ustar pbuserpbgroup00000000000000=== modified file 'http13client/client_test.go' --- http13client/client_test.go 2014-06-20 11:00:47 +0000 +++ http13client/client_test.go 2014-06-20 12:05:53 +0000 @@ -15,8 +15,8 @@ "fmt" "io" "io/ioutil" - "log" "net" + "net/http" . "launchpad.net/ubuntu-push/http13client" "net/http/httptest" "net/url" @@ -29,7 +29,7 @@ "time" ) -var robotsTxtHandler = HandlerFunc(func(w ResponseWriter, r *Request) { +var robotsTxtHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Last-Modified", "sometime") fmt.Fprintf(w, "User-agent: go\nDisallow: /something/") }) @@ -195,7 +195,7 @@ func TestClientRedirects(t *testing.T) { defer afterTest(t) var ts *httptest.Server - ts = httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n, _ := strconv.Atoi(r.FormValue("n")) // Test Referer header. (7 is arbitrary position to test at) if n == 7 { @@ -204,7 +204,7 @@ } } if n < 15 { - Redirect(w, r, fmt.Sprintf("/?n=%d", n+1), StatusFound) + http.Redirect(w, r, fmt.Sprintf("/?n=%d", n+1), StatusFound) return } fmt.Fprintf(w, "n=%d", n) @@ -273,7 +273,7 @@ bytes.Buffer } var ts *httptest.Server - ts = httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Lock() fmt.Fprintf(&log.Buffer, "%s %s ", r.Method, r.RequestURI) log.Unlock() @@ -314,21 +314,21 @@ } } -var expectedCookies = []*Cookie{ +var expectedCookies = []*http.Cookie{ {Name: "ChocolateChip", Value: "tasty"}, {Name: "First", Value: "Hit"}, {Name: "Second", Value: "Hit"}, } -var echoCookiesRedirectHandler = HandlerFunc(func(w ResponseWriter, r *Request) { +var echoCookiesRedirectHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for _, cookie := range r.Cookies() { - SetCookie(w, cookie) + http.SetCookie(w, cookie) } if r.URL.Path == "/" { - SetCookie(w, expectedCookies[1]) - Redirect(w, r, "/second", StatusMovedPermanently) + http.SetCookie(w, expectedCookies[1]) + http.Redirect(w, r, "/second", StatusMovedPermanently) } else { - SetCookie(w, expectedCookies[2]) + http.SetCookie(w, expectedCookies[2]) w.Write([]byte("hello")) } }) @@ -336,7 +336,7 @@ func TestClientSendsCookieFromJar(t *testing.T) { tr := &recordingTransport{} client := &Client{Transport: tr} - client.Jar = &TestJar{perURL: make(map[string][]*Cookie)} + client.Jar = &TestJar{perURL: make(map[string][]*http.Cookie)} us := "http://dummy.faketld/" u, _ := url.Parse(us) client.Jar.SetCookies(u, expectedCookies) @@ -366,19 +366,19 @@ // scope of all cookies. type TestJar struct { m sync.Mutex - perURL map[string][]*Cookie + perURL map[string][]*http.Cookie } -func (j *TestJar) SetCookies(u *url.URL, cookies []*Cookie) { +func (j *TestJar) SetCookies(u *url.URL, cookies []*http.Cookie) { j.m.Lock() defer j.m.Unlock() if j.perURL == nil { - j.perURL = make(map[string][]*Cookie) + j.perURL = make(map[string][]*http.Cookie) } j.perURL[u.Host] = cookies } -func (j *TestJar) Cookies(u *url.URL) []*Cookie { +func (j *TestJar) Cookies(u *url.URL) []*http.Cookie { j.m.Lock() defer j.m.Unlock() return j.perURL[u.Host] @@ -393,7 +393,7 @@ Jar: new(TestJar), } u, _ := url.Parse(ts.URL) - c.Jar.SetCookies(u, []*Cookie{expectedCookies[0]}) + c.Jar.SetCookies(u, []*http.Cookie{expectedCookies[0]}) resp, err := c.Get(ts.URL) if err != nil { t.Fatalf("Get: %v", err) @@ -402,7 +402,7 @@ matchReturnedCookies(t, expectedCookies, resp.Cookies()) } -func matchReturnedCookies(t *testing.T, expected, given []*Cookie) { +func matchReturnedCookies(t *testing.T, expected, given []*http.Cookie) { if len(given) != len(expected) { t.Logf("Received cookies: %v", given) t.Errorf("Expected %d cookies, got %d", len(expected), len(given)) @@ -423,14 +423,14 @@ func TestJarCalls(t *testing.T) { defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { pathSuffix := r.RequestURI[1:] if r.RequestURI == "/nosetcookie" { return // dont set cookies for this path } - SetCookie(w, &Cookie{Name: "name" + pathSuffix, Value: "val" + pathSuffix}) + http.SetCookie(w, &http.Cookie{Name: "name" + pathSuffix, Value: "val" + pathSuffix}) if r.RequestURI == "/" { - Redirect(w, r, "http://secondhost.fake/secondpath", 302) + http.Redirect(w, r, "http://secondhost.fake/secondpath", 302) } })) defer ts.Close() @@ -470,11 +470,11 @@ log bytes.Buffer } -func (j *RecordingJar) SetCookies(u *url.URL, cookies []*Cookie) { +func (j *RecordingJar) SetCookies(u *url.URL, cookies []*http.Cookie) { j.logf("SetCookie(%q, %v)\n", u, cookies) } -func (j *RecordingJar) Cookies(u *url.URL) []*Cookie { +func (j *RecordingJar) Cookies(u *url.URL) []*http.Cookie { j.logf("Cookies(%q)\n", u) return nil } @@ -488,11 +488,11 @@ func TestStreamingGet(t *testing.T) { defer afterTest(t) say := make(chan string) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - w.(Flusher).Flush() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.(http.Flusher).Flush() for str := range say { w.Write([]byte(str)) - w.(Flusher).Flush() + w.(http.Flusher).Flush() } })) defer ts.Close() @@ -538,7 +538,7 @@ // don't send a TCP packet per line of the http request + body. func TestClientWrites(t *testing.T) { defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() @@ -570,46 +570,6 @@ } } -func TestClientInsecureTransport(t *testing.T) { - defer afterTest(t) - ts := httptest.NewTLSServer(HandlerFunc(func(w ResponseWriter, r *Request) { - w.Write([]byte("Hello")) - })) - errc := make(chanWriter, 10) // but only expecting 1 - ts.Config.ErrorLog = log.New(errc, "", 0) - defer ts.Close() - - // TODO(bradfitz): add tests for skipping hostname checks too? - // would require a new cert for testing, and probably - // redundant with these tests. - for _, insecure := range []bool{true, false} { - tr := &Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: insecure, - }, - } - defer tr.CloseIdleConnections() - c := &Client{Transport: tr} - res, err := c.Get(ts.URL) - if (err == nil) != insecure { - t.Errorf("insecure=%v: got unexpected err=%v", insecure, err) - } - if res != nil { - res.Body.Close() - } - } - - select { - case v := <-errc: - if !strings.Contains(v, "TLS handshake error") { - t.Errorf("expected an error log message containing 'TLS handshake error'; got %q", v) - } - case <-time.After(5 * time.Second): - t.Errorf("timeout waiting for logged error") - } - -} - func TestClientErrorWithRequestURI(t *testing.T) { defer afterTest(t) req, _ := NewRequest("GET", "http://localhost:1234/", nil) @@ -641,7 +601,7 @@ func TestClientWithCorrectTLSServerName(t *testing.T) { defer afterTest(t) - ts := httptest.NewTLSServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.TLS.ServerName != "127.0.0.1" { t.Errorf("expected client to set ServerName 127.0.0.1, got: %q", r.TLS.ServerName) } @@ -654,33 +614,6 @@ } } -func TestClientWithIncorrectTLSServerName(t *testing.T) { - defer afterTest(t) - ts := httptest.NewTLSServer(HandlerFunc(func(w ResponseWriter, r *Request) {})) - defer ts.Close() - errc := make(chanWriter, 10) // but only expecting 1 - ts.Config.ErrorLog = log.New(errc, "", 0) - - trans := newTLSTransport(t, ts) - trans.TLSClientConfig.ServerName = "badserver" - c := &Client{Transport: trans} - _, err := c.Get(ts.URL) - if err == nil { - t.Fatalf("expected an error") - } - if !strings.Contains(err.Error(), "127.0.0.1") || !strings.Contains(err.Error(), "badserver") { - t.Errorf("wanted error mentioning 127.0.0.1 and badserver; got error: %v", err) - } - select { - case v := <-errc: - if !strings.Contains(v, "TLS handshake error") { - t.Errorf("expected an error log message containing 'TLS handshake error'; got %q", v) - } - case <-time.After(5 * time.Second): - t.Errorf("timeout waiting for logged error") - } -} - // Test for golang.org/issue/5829; the Transport should respect TLSClientConfig.ServerName // when not empty. // @@ -692,7 +625,7 @@ // The httptest.Server has a cert with "example.com" as its name. func TestTransportUsesTLSConfigServerName(t *testing.T) { defer afterTest(t) - ts := httptest.NewTLSServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello")) })) defer ts.Close() @@ -713,7 +646,7 @@ func TestResponseSetsTLSConnectionState(t *testing.T) { defer afterTest(t) - ts := httptest.NewTLSServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello")) })) defer ts.Close() @@ -741,7 +674,7 @@ // Verify Response.ContentLength is populated. http://golang.org/issue/4126 func TestClientHeadContentLength(t *testing.T) { defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if v := r.FormValue("cl"); v != "" { w.Header().Set("Content-Length", v) } @@ -777,7 +710,7 @@ func TestEmptyPasswordAuth(t *testing.T) { defer afterTest(t) gopher := "gopher" - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if strings.HasPrefix(auth, "Basic ") { encoded := auth[6:] @@ -849,15 +782,15 @@ defer afterTest(t) sawRoot := make(chan bool, 1) sawSlow := make(chan bool, 1) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { sawRoot <- true - Redirect(w, r, "/slow", StatusFound) + http.Redirect(w, r, "/slow", StatusFound) return } if r.URL.Path == "/slow" { w.Write([]byte("Hello")) - w.(Flusher).Flush() + w.(http.Flusher).Flush() sawSlow <- true time.Sleep(2 * time.Second) return @@ -910,10 +843,10 @@ func TestClientRedirectEatsBody(t *testing.T) { defer afterTest(t) saw := make(chan string, 2) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { saw <- r.RemoteAddr if r.URL.Path == "/" { - Redirect(w, r, "/foo", StatusFound) // which includes a body + http.Redirect(w, r, "/foo", StatusFound) // which includes a body } })) defer ts.Close() @@ -957,7 +890,7 @@ func TestClientTrailers(t *testing.T) { defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Connection", "close") w.Header().Set("Trailer", "Server-Trailer-A, Server-Trailer-B") w.Header().Add("Trailer", "Server-Trailer-C") @@ -992,9 +925,9 @@ // trailers to be sent, if and only if they were // previously declared with w.Header().Set("Trailer", // ..keys..) - w.(Flusher).Flush() - conn, buf, _ := w.(Hijacker).Hijack() - t := Header{} + w.(http.Flusher).Flush() + conn, buf, _ := w.(http.Hijacker).Hijack() + t := http.Header{} t.Set("Server-Trailer-A", "valuea") t.Set("Server-Trailer-C", "valuec") // skipping B buf.WriteString("0\r\n") // eof @@ -1015,7 +948,7 @@ req.Trailer["Client-Trailer-B"] = []string{"valueb"} }), )) - req.Trailer = Header{ + req.Trailer = http.Header{ "Client-Trailer-A": nil, // to be set later "Client-Trailer-B": nil, // to be set later } @@ -1027,7 +960,7 @@ if err := wantBody(res, err, "decl: [Client-Trailer-A Client-Trailer-B], vals: valuea, valueb"); err != nil { t.Error(err) } - want := Header{ + want := http.Header{ "Server-Trailer-A": []string{"valuea"}, "Server-Trailer-B": nil, "Server-Trailer-C": []string{"valuec"}, === modified file 'http13client/export_test.go' --- http13client/export_test.go 2014-06-20 11:00:47 +0000 +++ http13client/export_test.go 2014-06-20 12:00:22 +0000 @@ -9,15 +9,12 @@ import ( "net" - "time" ) func NewLoggingConn(baseName string, c net.Conn) net.Conn { return newLoggingConn(baseName, c) } -var ExportAppendTime = appendTime - func (t *Transport) NumPendingRequestsForTesting() int { t.reqMu.Lock() defer t.reqMu.Unlock() @@ -57,13 +54,6 @@ return len(t.idleConnCh) } -func NewTestTimeoutHandler(handler Handler, ch <-chan time.Time) Handler { - f := func() <-chan time.Time { - return ch - } - return &timeoutHandler{handler, f, ""} -} - func ResetCachedEnvironment() { httpProxyEnv.reset() noProxyEnv.reset() === modified file 'http13client/header_test.go' --- http13client/header_test.go 2014-06-20 11:00:47 +0000 +++ http13client/header_test.go 2014-06-20 12:00:22 +0000 @@ -6,19 +6,20 @@ import ( "bytes" + "net/http" "runtime" "testing" "time" ) var headerWriteTests = []struct { - h Header + h http.Header exclude map[string]bool expected string }{ - {Header{}, nil, ""}, + {http.Header{}, nil, ""}, { - Header{ + http.Header{ "Content-Type": {"text/html; charset=UTF-8"}, "Content-Length": {"0"}, }, @@ -26,14 +27,14 @@ "Content-Length: 0\r\nContent-Type: text/html; charset=UTF-8\r\n", }, { - Header{ + http.Header{ "Content-Length": {"0", "1", "2"}, }, nil, "Content-Length: 0\r\nContent-Length: 1\r\nContent-Length: 2\r\n", }, { - Header{ + http.Header{ "Expires": {"-1"}, "Content-Length": {"0"}, "Content-Encoding": {"gzip"}, @@ -42,7 +43,7 @@ "Content-Encoding: gzip\r\nExpires: -1\r\n", }, { - Header{ + http.Header{ "Expires": {"-1"}, "Content-Length": {"0", "1", "2"}, "Content-Encoding": {"gzip"}, @@ -51,7 +52,7 @@ "Content-Encoding: gzip\r\nExpires: -1\r\n", }, { - Header{ + http.Header{ "Expires": {"-1"}, "Content-Length": {"0"}, "Content-Encoding": {"gzip"}, @@ -60,7 +61,7 @@ "", }, { - Header{ + http.Header{ "Nil": nil, "Empty": {}, "Blank": {""}, @@ -71,7 +72,7 @@ }, // Tests header sorting when over the insertion sort threshold side: { - Header{ + http.Header{ "k1": {"1a", "1b"}, "k2": {"2a", "2b"}, "k3": {"3a", "3b"}, @@ -101,21 +102,21 @@ } var parseTimeTests = []struct { - h Header + h http.Header err bool }{ - {Header{"Date": {""}}, true}, - {Header{"Date": {"invalid"}}, true}, - {Header{"Date": {"1994-11-06T08:49:37Z00:00"}}, true}, - {Header{"Date": {"Sun, 06 Nov 1994 08:49:37 GMT"}}, false}, - {Header{"Date": {"Sunday, 06-Nov-94 08:49:37 GMT"}}, false}, - {Header{"Date": {"Sun Nov 6 08:49:37 1994"}}, false}, + {http.Header{"Date": {""}}, true}, + {http.Header{"Date": {"invalid"}}, true}, + {http.Header{"Date": {"1994-11-06T08:49:37Z00:00"}}, true}, + {http.Header{"Date": {"Sun, 06 Nov 1994 08:49:37 GMT"}}, false}, + {http.Header{"Date": {"Sunday, 06-Nov-94 08:49:37 GMT"}}, false}, + {http.Header{"Date": {"Sun Nov 6 08:49:37 1994"}}, false}, } func TestParseTime(t *testing.T) { expect := time.Date(1994, 11, 6, 8, 49, 37, 0, time.UTC) for i, test := range parseTimeTests { - d, err := ParseTime(test.h.Get("Date")) + d, err := http.ParseTime(test.h.Get("Date")) if err != nil { if !test.err { t.Errorf("#%d:\n got err: %v", i, err) @@ -175,7 +176,7 @@ } } -var testHeader = Header{ +var testHeader = http.Header{ "Content-Length": {"123"}, "Content-Type": {"text/plain"}, "Date": {"some date at some time Z"}, @@ -196,9 +197,9 @@ if testing.Short() { t.Skip("skipping alloc test in short mode") } - if raceEnabled { + /*if raceEnabled { t.Skip("skipping test under race detector") - } + }*/ if runtime.GOMAXPROCS(0) > 1 { t.Skip("skipping; GOMAXPROCS>1") } === modified file 'http13client/npn_test.go' --- http13client/npn_test.go 2014-06-20 11:00:47 +0000 +++ http13client/npn_test.go 2014-06-20 12:05:53 +0000 @@ -11,13 +11,14 @@ "io" "io/ioutil" . "launchpad.net/ubuntu-push/http13client" + "net/http" "net/http/httptest" "strings" "testing" ) func TestNextProtoUpgrade(t *testing.T) { - ts := httptest.NewUnstartedServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "path=%s,proto=", r.URL.Path) if r.TLS != nil { w.Write([]byte(r.TLS.NegotiatedProtocol)) @@ -32,7 +33,7 @@ ts.TLS = &tls.Config{ NextProtos: []string{"unhandled-proto", "tls-0.9"}, } - ts.Config.TLSNextProto = map[string]func(*Server, *tls.Conn, Handler){ + ts.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){ "tls-0.9": handleTLSProtocol09, } ts.StartTLS() @@ -90,7 +91,7 @@ // handleTLSProtocol09 implements the HTTP/0.9 protocol over TLS, for the // TestNextProtoUpgrade test. -func handleTLSProtocol09(srv *Server, conn *tls.Conn, h Handler) { +func handleTLSProtocol09(srv *http.Server, conn *tls.Conn, h http.Handler) { br := bufio.NewReader(conn) line, err := br.ReadString('\n') if err != nil { @@ -101,18 +102,18 @@ if path == line { return } - req, _ := NewRequest("GET", path, nil) + req, _ := http.NewRequest("GET", path, nil) req.Proto = "HTTP/0.9" req.ProtoMajor = 0 req.ProtoMinor = 9 - rw := &http09Writer{conn, make(Header)} + rw := &http09Writer{conn, make(http.Header)} h.ServeHTTP(rw, req) } type http09Writer struct { io.Writer - h Header + h http.Header } -func (w http09Writer) Header() Header { return w.h } +func (w http09Writer) Header() http.Header { return w.h } func (w http09Writer) WriteHeader(int) {} // no headers === modified file 'http13client/readrequest_test.go' --- http13client/readrequest_test.go 2014-06-20 11:00:47 +0000 +++ http13client/readrequest_test.go 2014-06-20 12:00:22 +0000 @@ -9,6 +9,7 @@ "bytes" "fmt" "io" + "net/http" "net/url" "reflect" "testing" @@ -18,13 +19,13 @@ Raw string Req *Request Body string - Trailer Header + Trailer http.Header Error string } var noError = "" var noBody = "" -var noTrailer Header = nil +var noTrailer http.Header = nil var reqTests = []reqTest{ // Baseline test; All Request fields included for template use @@ -51,7 +52,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{ + Header: http.Header{ "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, "Accept-Language": {"en-us,en;q=0.5"}, "Accept-Encoding": {"gzip,deflate"}, @@ -86,7 +87,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, Close: false, ContentLength: 0, Host: "foo.com", @@ -112,7 +113,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, Close: false, ContentLength: 0, Host: "test", @@ -163,14 +164,14 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, ContentLength: -1, Host: "foo.com", RequestURI: "/", }, "foobar", - Header{ + http.Header{ "Trailer-Key": {"Trailer-Value"}, }, noError, @@ -188,7 +189,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, Close: false, ContentLength: 0, Host: "www.google.com:443", @@ -212,7 +213,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, Close: false, ContentLength: 0, Host: "127.0.0.1:6060", @@ -236,7 +237,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, Close: false, ContentLength: 0, Host: "", @@ -259,7 +260,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{ + Header: http.Header{ "Server": []string{"foo"}, }, Close: false, @@ -283,7 +284,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{ + Header: http.Header{ "Server": []string{"foo"}, }, Close: false, === modified file 'http13client/request_test.go' --- http13client/request_test.go 2014-06-20 11:00:47 +0000 +++ http13client/request_test.go 2014-06-20 12:05:53 +0000 @@ -12,6 +12,7 @@ "io/ioutil" "mime/multipart" . "launchpad.net/ubuntu-push/http13client" + "net/http" "net/http/httptest" "net/url" "os" @@ -110,7 +111,7 @@ for i, test := range parseContentTypeTests { req := &Request{ Method: "POST", - Header: Header(test.contentType), + Header: http.Header(test.contentType), Body: ioutil.NopCloser(strings.NewReader("body")), } err := req.ParseForm() @@ -143,7 +144,7 @@ func TestMultipartReader(t *testing.T) { req := &Request{ Method: "POST", - Header: Header{"Content-Type": {`multipart/form-data; boundary="foo123"`}}, + Header: http.Header{"Content-Type": {`multipart/form-data; boundary="foo123"`}}, Body: ioutil.NopCloser(new(bytes.Buffer)), } multipart, err := req.MultipartReader() @@ -151,7 +152,7 @@ t.Errorf("expected multipart; error: %v", err) } - req.Header = Header{"Content-Type": {"text/plain"}} + req.Header = http.Header{"Content-Type": {"text/plain"}} multipart, err = req.MultipartReader() if multipart != nil { t.Error("unexpected multipart for text/plain") @@ -161,7 +162,7 @@ func TestParseMultipartForm(t *testing.T) { req := &Request{ Method: "POST", - Header: Header{"Content-Type": {`multipart/form-data; boundary="foo123"`}}, + Header: http.Header{"Content-Type": {`multipart/form-data; boundary="foo123"`}}, Body: ioutil.NopCloser(new(bytes.Buffer)), } err := req.ParseMultipartForm(25) @@ -169,7 +170,7 @@ t.Error("expected multipart EOF, got nil") } - req.Header = Header{"Content-Type": {"text/plain"}} + req.Header = http.Header{"Content-Type": {"text/plain"}} err = req.ParseMultipartForm(25) if err != ErrNotMultipart { t.Error("expected ErrNotMultipart for text/plain") @@ -177,7 +178,7 @@ } func TestRedirect(t *testing.T) { - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": w.Header().Set("Location", "/foo/") === modified file 'http13client/requestwrite_test.go' --- http13client/requestwrite_test.go 2014-06-20 11:00:47 +0000 +++ http13client/requestwrite_test.go 2014-06-20 12:00:22 +0000 @@ -10,6 +10,7 @@ "fmt" "io" "io/ioutil" + "net/http" "net/url" "strings" "testing" @@ -39,7 +40,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{ + Header: http.Header{ "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, "Accept-Charset": {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"}, "Accept-Encoding": {"gzip,deflate"}, @@ -85,7 +86,7 @@ }, ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, TransferEncoding: []string{"chunked"}, }, @@ -114,7 +115,7 @@ }, ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, Close: true, TransferEncoding: []string{"chunked"}, }, @@ -147,7 +148,7 @@ }, ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, Close: true, ContentLength: 6, }, @@ -177,7 +178,7 @@ Method: "POST", URL: mustParseURL("http://example.com/"), Host: "example.com", - Header: Header{ + Header: http.Header{ "Content-Length": []string{"10"}, // ignored }, ContentLength: 6, @@ -358,7 +359,7 @@ URL: mustParseURL("/foo"), ProtoMajor: 1, ProtoMinor: 0, - Header: Header{ + Header: http.Header{ "X-Foo": []string{"X-Bar"}, }, }, @@ -384,7 +385,7 @@ }, ProtoMajor: 1, ProtoMinor: 1, - Header: Header{ + Header: http.Header{ "Host": []string{"bad.example.com"}, }, }, @@ -405,7 +406,7 @@ }, ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, }, WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" + @@ -424,7 +425,7 @@ }, ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, }, WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" + @@ -444,7 +445,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{ + Header: http.Header{ "ALL-CAPS": {"x"}, }, }, @@ -474,7 +475,7 @@ } setBody() if tt.Req.Header == nil { - tt.Req.Header = make(Header) + tt.Req.Header = make(http.Header) } var braw bytes.Buffer === modified file 'http13client/response_test.go' --- http13client/response_test.go 2014-06-20 11:00:47 +0000 +++ http13client/response_test.go 2014-06-20 12:05:53 +0000 @@ -12,6 +12,7 @@ "fmt" "io" "io/ioutil" + "net/http" "net/url" "reflect" "regexp" @@ -48,7 +49,7 @@ ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), - Header: Header{ + Header: http.Header{ "Connection": {"close"}, // TODO(rsc): Delete? }, Close: true, @@ -71,7 +72,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, Request: dummyReq("GET"), Close: true, ContentLength: -1, @@ -92,7 +93,7 @@ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, - Header: Header{}, + Header: http.Header{}, Request: dummyReq("GET"), Close: false, ContentLength: 0, @@ -116,7 +117,7 @@ ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), - Header: Header{ + Header: http.Header{ "Connection": {"close"}, "Content-Length": {"10"}, }, @@ -146,7 +147,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), - Header: Header{}, + Header: http.Header{}, Close: false, ContentLength: -1, TransferEncoding: []string{"chunked"}, @@ -173,7 +174,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), - Header: Header{}, + Header: http.Header{}, Close: false, ContentLength: -1, TransferEncoding: []string{"chunked"}, @@ -195,7 +196,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("HEAD"), - Header: Header{}, + Header: http.Header{}, TransferEncoding: []string{"chunked"}, Close: false, ContentLength: -1, @@ -217,7 +218,7 @@ ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("HEAD"), - Header: Header{"Content-Length": {"256"}}, + Header: http.Header{"Content-Length": {"256"}}, TransferEncoding: nil, Close: true, ContentLength: 256, @@ -239,7 +240,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("HEAD"), - Header: Header{"Content-Length": {"256"}}, + Header: http.Header{"Content-Length": {"256"}}, TransferEncoding: nil, Close: false, ContentLength: 256, @@ -260,7 +261,7 @@ ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("HEAD"), - Header: Header{}, + Header: http.Header{}, TransferEncoding: nil, Close: true, ContentLength: -1, @@ -282,7 +283,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), - Header: Header{ + Header: http.Header{ "Content-Length": {"0"}, }, Close: false, @@ -303,7 +304,7 @@ ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), - Header: Header{}, + Header: http.Header{}, Close: true, ContentLength: -1, }, @@ -322,7 +323,7 @@ ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), - Header: Header{}, + Header: http.Header{}, Close: true, ContentLength: -1, }, @@ -344,7 +345,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), - Header: Header{ + Header: http.Header{ "Content-Type": []string{"multipart/byteranges; boundary=18a75608c8f47cef"}, }, Close: true, @@ -367,7 +368,7 @@ Proto: "HTTP/1.0", ProtoMajor: 1, ProtoMinor: 0, - Header: Header{ + Header: http.Header{ "Connection": {"close"}, // TODO(rsc): Delete? }, Close: true, @@ -549,7 +550,7 @@ func TestLocationResponse(t *testing.T) { for i, tt := range responseLocationTests { res := new(Response) - res.Header = make(Header) + res.Header = make(http.Header) res.Header.Set("Location", tt.location) if tt.requrl != "" { res.Request = &Request{} @@ -630,16 +631,3 @@ t.Errorf("ReadResponse = %v; want io.ErrUnexpectedEOF", err) } } - -func TestNeedsSniff(t *testing.T) { - // needsSniff returns true with an empty response. - r := &response{} - if got, want := r.needsSniff(), true; got != want { - t.Errorf("needsSniff = %t; want %t", got, want) - } - // needsSniff returns false when Content-Type = nil. - r.handlerHeader = Header{"Content-Type": nil} - if got, want := r.needsSniff(), false; got != want { - t.Errorf("needsSniff empty Content-Type = %t; want %t", got, want) - } -} === modified file 'http13client/responsewrite_test.go' --- http13client/responsewrite_test.go 2014-06-20 11:00:47 +0000 +++ http13client/responsewrite_test.go 2014-06-20 12:05:53 +0000 @@ -7,6 +7,7 @@ import ( "bytes" "io/ioutil" + "net/http" "strings" "testing" ) @@ -25,7 +26,7 @@ ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), - Header: Header{}, + Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: 6, }, @@ -41,7 +42,7 @@ ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), - Header: Header{}, + Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: -1, }, @@ -56,7 +57,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), - Header: Header{}, + Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: -1, Close: true, @@ -73,7 +74,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq11("GET"), - Header: Header{}, + Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: -1, Close: false, @@ -91,7 +92,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq11("GET"), - Header: Header{}, + Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: -1, TransferEncoding: []string{"chunked"}, @@ -108,7 +109,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq11("GET"), - Header: Header{}, + Header: http.Header{}, Body: nil, ContentLength: 0, Close: false, @@ -124,7 +125,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq11("GET"), - Header: Header{}, + Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("")), ContentLength: 0, Close: false, @@ -140,7 +141,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq11("GET"), - Header: Header{}, + Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("foo")), ContentLength: 0, Close: false, @@ -156,7 +157,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), - Header: Header{}, + Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: 6, TransferEncoding: []string{"chunked"}, @@ -177,7 +178,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), - Header: Header{ + Header: http.Header{ "Foo": []string{" Bar\nBaz "}, }, Body: nil, @@ -200,7 +201,7 @@ ProtoMajor: 1, ProtoMinor: 1, Request: &Request{Method: "POST"}, - Header: Header{}, + Header: http.Header{}, ContentLength: 0, TransferEncoding: nil, Body: nil, === modified file 'http13client/transport_test.go' --- http13client/transport_test.go 2014-06-20 11:00:47 +0000 +++ http13client/transport_test.go 2014-06-20 12:05:53 +0000 @@ -18,8 +18,8 @@ "io/ioutil" "log" "net" - "launchpad.net/ubuntu-push/http13client" . "launchpad.net/ubuntu-push/http13client" + "net/http" "net/http/httptest" "net/url" "os" @@ -35,7 +35,7 @@ // and then verify that the final 2 responses get errors back. // hostPortHandler writes back the client's "host:port". -var hostPortHandler = HandlerFunc(func(w ResponseWriter, r *Request) { +var hostPortHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.FormValue("close") == "true" { w.Header().Set("Connection", "close") } @@ -289,7 +289,7 @@ const msg = "foobar" var addrSeen map[string]int - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { addrSeen[r.RemoteAddr]++ if r.URL.Path == "/chunked/" { w.WriteHeader(200) @@ -308,7 +308,7 @@ wantLen := []int{len(msg), -1}[pi] addrSeen = make(map[string]int) for i := 0; i < 3; i++ { - res, err := http.Get(ts.URL + path) + res, err := Get(ts.URL + path) if err != nil { t.Errorf("Get %s: %v", path, err) continue @@ -338,7 +338,7 @@ defer afterTest(t) resch := make(chan string) gotReq := make(chan bool) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotReq <- true msg := <-resch _, err := w.Write([]byte(msg)) @@ -466,12 +466,12 @@ if testing.Short() { t.Skip("skipping test in short mode") } - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", "5") w.Header().Set("Content-Type", "text/plain") w.Write([]byte("Hello")) - w.(Flusher).Flush() - conn, buf, _ := w.(Hijacker).Hijack() + w.(http.Flusher).Flush() + conn, buf, _ := w.(http.Hijacker).Hijack() buf.Flush() conn.Close() })) @@ -519,7 +519,7 @@ // with no bodies properly func TestTransportHeadResponses(t *testing.T) { defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "HEAD" { panic("expected HEAD; got " + r.Method) } @@ -554,7 +554,7 @@ // on responses to HEAD requests. func TestTransportHeadChunkedResponse(t *testing.T) { defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "HEAD" { panic("expected HEAD; got " + r.Method) } @@ -597,7 +597,7 @@ func TestRoundTripGzip(t *testing.T) { defer afterTest(t) const responseBody = "test response body" - ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { accept := req.Header.Get("Accept-Encoding") if expect := req.FormValue("expect_accept"); accept != expect { t.Errorf("in handler, test %v: Accept-Encoding = %q, want %q", @@ -656,7 +656,7 @@ defer afterTest(t) const testString = "The test string aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" const nRandBytes = 1024 * 1024 - ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if req.Method == "HEAD" { if g := req.Header.Get("Accept-Encoding"); g != "" { t.Errorf("HEAD request sent with Accept-Encoding of %q; want none", g) @@ -751,11 +751,11 @@ func TestTransportProxy(t *testing.T) { defer afterTest(t) ch := make(chan string, 1) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ch <- "real server" })) defer ts.Close() - proxy := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ch <- "proxy for " + r.URL.String() })) defer proxy.Close() @@ -779,7 +779,7 @@ // Content-Encoding is removed. func TestTransportGzipRecursive(t *testing.T) { defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Encoding", "gzip") w.Write(rgz) })) @@ -807,7 +807,7 @@ // a short gzip body func TestTransportGzipShort(t *testing.T) { defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Encoding", "gzip") w.Write([]byte{0x1f, 0x8b}) })) @@ -838,7 +838,7 @@ defer afterTest(t) gotReqCh := make(chan bool) unblockCh := make(chan bool) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotReqCh <- true <-unblockCh w.Header().Set("Content-Length", "0") @@ -905,7 +905,7 @@ t.Skip("skipping test; see http://golang.org/issue/7237") } defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() @@ -948,7 +948,7 @@ c := &Client{Transport: tr} unblockCh := make(chan bool, 1) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-unblockCh tr.CloseIdleConnections() })) @@ -975,7 +975,7 @@ func TestIssue3644(t *testing.T) { defer afterTest(t) const numFoos = 5000 - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Connection", "close") for i := 0; i < numFoos; i++ { w.Write([]byte("foo ")) @@ -1003,8 +1003,8 @@ func TestIssue3595(t *testing.T) { defer afterTest(t) const deniedMsg = "sorry, denied." - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { - Error(w, deniedMsg, StatusUnauthorized) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, deniedMsg, StatusUnauthorized) })) defer ts.Close() tr := &Transport{} @@ -1027,7 +1027,7 @@ // "client fails to handle requests with no body and chunked encoding" func TestChunkedNoContent(t *testing.T) { defer afterTest(t) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(StatusNoContent) })) defer ts.Close() @@ -1055,7 +1055,7 @@ maxProcs, numReqs = 4, 50 } defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(maxProcs)) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "%v", r.FormValue("echo")) })) defer ts.Close() @@ -1116,8 +1116,8 @@ } defer afterTest(t) const debug = false - mux := NewServeMux() - mux.HandleFunc("/get", func(w ResponseWriter, r *Request) { + mux := http.NewServeMux() + mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { io.Copy(w, neverEnding('a')) }) ts := httptest.NewServer(mux) @@ -1180,11 +1180,11 @@ } defer afterTest(t) const debug = false - mux := NewServeMux() - mux.HandleFunc("/get", func(w ResponseWriter, r *Request) { + mux := http.NewServeMux() + mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { io.Copy(w, neverEnding('a')) }) - mux.HandleFunc("/put", func(w ResponseWriter, r *Request) { + mux.HandleFunc("/put", func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() io.Copy(ioutil.Discard, r.Body) }) @@ -1251,11 +1251,11 @@ t.Skip("skipping timeout test in -short mode") } inHandler := make(chan bool, 1) - mux := NewServeMux() - mux.HandleFunc("/fast", func(w ResponseWriter, r *Request) { + mux := http.NewServeMux() + mux.HandleFunc("/fast", func(w http.ResponseWriter, r *http.Request) { inHandler <- true }) - mux.HandleFunc("/slow", func(w ResponseWriter, r *Request) { + mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) { inHandler <- true time.Sleep(2 * time.Second) }) @@ -1322,9 +1322,9 @@ t.Skip("skipping test in -short mode") } unblockc := make(chan bool) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello") - w.(Flusher).Flush() // send headers and some body + w.(http.Flusher).Flush() // send headers and some body <-unblockc })) defer ts.Close() @@ -1431,14 +1431,14 @@ defer afterTest(t) writeErr := make(chan error, 1) msg := []byte("young\n") - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for { _, err := w.Write(msg) if err != nil { writeErr <- err return } - w.(Flusher).Flush() + w.(http.Flusher).Flush() } })) defer ts.Close() @@ -1494,7 +1494,7 @@ res := &Response{ Status: "200 OK", StatusCode: 200, - Header: make(Header), + Header: make(http.Header), Body: ioutil.NopCloser(strings.NewReader("You wanted " + req.URL.String())), } return res, nil @@ -1523,7 +1523,7 @@ defer afterTest(t) tr := &Transport{} _, err := tr.RoundTrip(&Request{ - Header: make(Header), + Header: make(http.Header), URL: &url.URL{ Scheme: "http", }, @@ -1537,14 +1537,14 @@ func TestTransportSocketLateBinding(t *testing.T) { defer afterTest(t) - mux := NewServeMux() + mux := http.NewServeMux() fooGate := make(chan bool, 1) - mux.HandleFunc("/foo", func(w ResponseWriter, r *Request) { + mux.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("foo-ipport", r.RemoteAddr) - w.(Flusher).Flush() + w.(http.Flusher).Flush() <-fooGate }) - mux.HandleFunc("/bar", func(w ResponseWriter, r *Request) { + mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("bar-ipport", r.RemoteAddr) }) ts := httptest.NewServer(mux) @@ -1767,7 +1767,7 @@ var mu sync.Mutex var n int - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() n++ mu.Unlock() @@ -1803,7 +1803,7 @@ // then closes it. func TestTransportClosesRequestBody(t *testing.T) { defer afterTest(t) - ts := httptest.NewServer(http.HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.Copy(ioutil.Discard, r.Body) })) defer ts.Close() @@ -1890,9 +1890,9 @@ t.Skip("skipping flaky test on Windows; golang.org/issue/7634") } closedc := make(chan bool, 1) - ts := httptest.NewTLSServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/keep-alive-then-die") { - conn, _, _ := w.(Hijacker).Hijack() + conn, _, _ := w.(http.Hijacker).Hijack() conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nfoo")) conn.Close() closedc <- true @@ -1994,12 +1994,12 @@ } defer closeConn() - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { io.WriteString(w, "bar") return } - conn, _, _ := w.(Hijacker).Hijack() + conn, _, _ := w.(http.Hijacker).Hijack() sconn.Lock() sconn.c = conn sconn.Unlock() @@ -2056,7 +2056,7 @@ } defer afterTest(t) readBody := make(chan error, 1) - ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := ioutil.ReadAll(r.Body) readBody <- err })) @@ -2098,7 +2098,7 @@ } } -func wantBody(res *http.Response, err error, want string) error { +func wantBody(res *Response, err error, want string) error { if err != nil { return err } ubuntu-push-0.68+16.04.20160310.2/http13client/npn_test.go0000644000015600001650000000554312670364255023167 0ustar pbuserpbgroup00000000000000// Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http_test import ( "bufio" "crypto/tls" "fmt" "io" "io/ioutil" . "launchpad.net/ubuntu-push/http13client" "net/http" "net/http/httptest" "strings" "testing" ) func TestNextProtoUpgrade(t *testing.T) { ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "path=%s,proto=", r.URL.Path) if r.TLS != nil { w.Write([]byte(r.TLS.NegotiatedProtocol)) } if r.RemoteAddr == "" { t.Error("request with no RemoteAddr") } if r.Body == nil { t.Errorf("request with nil Body") } })) ts.TLS = &tls.Config{ NextProtos: []string{"unhandled-proto", "tls-0.9"}, } ts.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){ "tls-0.9": handleTLSProtocol09, } ts.StartTLS() defer ts.Close() tr := newTLSTransport(t, ts) defer tr.CloseIdleConnections() c := &Client{Transport: tr} // Normal request, without NPN. { res, err := c.Get(ts.URL) if err != nil { t.Fatal(err) } body, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatal(err) } if want := "path=/,proto="; string(body) != want { t.Errorf("plain request = %q; want %q", body, want) } } // Request to an advertised but unhandled NPN protocol. // Server will hang up. { tr.CloseIdleConnections() tr.TLSClientConfig.NextProtos = []string{"unhandled-proto"} _, err := c.Get(ts.URL) if err == nil { t.Errorf("expected error on unhandled-proto request") } } // Request using the "tls-0.9" protocol, which we register here. // It is HTTP/0.9 over TLS. { tlsConfig := newTLSTransport(t, ts).TLSClientConfig tlsConfig.NextProtos = []string{"tls-0.9"} conn, err := tls.Dial("tcp", ts.Listener.Addr().String(), tlsConfig) if err != nil { t.Fatal(err) } conn.Write([]byte("GET /foo\n")) body, err := ioutil.ReadAll(conn) if err != nil { t.Fatal(err) } if want := "path=/foo,proto=tls-0.9"; string(body) != want { t.Errorf("plain request = %q; want %q", body, want) } } } // handleTLSProtocol09 implements the HTTP/0.9 protocol over TLS, for the // TestNextProtoUpgrade test. func handleTLSProtocol09(srv *http.Server, conn *tls.Conn, h http.Handler) { br := bufio.NewReader(conn) line, err := br.ReadString('\n') if err != nil { return } line = strings.TrimSpace(line) path := strings.TrimPrefix(line, "GET ") if path == line { return } req, _ := http.NewRequest("GET", path, nil) req.Proto = "HTTP/0.9" req.ProtoMajor = 0 req.ProtoMinor = 9 rw := &http09Writer{conn, make(http.Header)} h.ServeHTTP(rw, req) } type http09Writer struct { io.Writer h http.Header } func (w http09Writer) Header() http.Header { return w.h } func (w http09Writer) WriteHeader(int) {} // no headers ubuntu-push-0.68+16.04.20160310.2/http13client/responsewrite_test.go0000644000015600001650000001266412670364255025307 0ustar pbuserpbgroup00000000000000// Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "bytes" "io/ioutil" "net/http" "strings" "testing" ) type respWriteTest struct { Resp Response Raw string } func TestResponseWrite(t *testing.T) { respWriteTests := []respWriteTest{ // HTTP/1.0, identity coding; no trailer { Response{ StatusCode: 503, ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: 6, }, "HTTP/1.0 503 Service Unavailable\r\n" + "Content-Length: 6\r\n\r\n" + "abcdef", }, // Unchunked response without Content-Length. { Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 0, Request: dummyReq("GET"), Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: -1, }, "HTTP/1.0 200 OK\r\n" + "\r\n" + "abcdef", }, // HTTP/1.1 response with unknown length and Connection: close { Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: -1, Close: true, }, "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n" + "abcdef", }, // HTTP/1.1 response with unknown length and not setting connection: close { Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq11("GET"), Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: -1, Close: false, }, "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\n" + "abcdef", }, // HTTP/1.1 response with unknown length and not setting connection: close, but // setting chunked. { Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq11("GET"), Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: -1, TransferEncoding: []string{"chunked"}, Close: false, }, "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + "6\r\nabcdef\r\n0\r\n\r\n", }, // HTTP/1.1 response 0 content-length, and nil body { Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq11("GET"), Header: http.Header{}, Body: nil, ContentLength: 0, Close: false, }, "HTTP/1.1 200 OK\r\n" + "Content-Length: 0\r\n" + "\r\n", }, // HTTP/1.1 response 0 content-length, and non-nil empty body { Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq11("GET"), Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("")), ContentLength: 0, Close: false, }, "HTTP/1.1 200 OK\r\n" + "Content-Length: 0\r\n" + "\r\n", }, // HTTP/1.1 response 0 content-length, and non-nil non-empty body { Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq11("GET"), Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("foo")), ContentLength: 0, Close: false, }, "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "\r\nfoo", }, // HTTP/1.1, chunked coding; empty trailer; close { Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), Header: http.Header{}, Body: ioutil.NopCloser(strings.NewReader("abcdef")), ContentLength: 6, TransferEncoding: []string{"chunked"}, Close: true, }, "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + "6\r\nabcdef\r\n0\r\n\r\n", }, // Header value with a newline character (Issue 914). // Also tests removal of leading and trailing whitespace. { Response{ StatusCode: 204, ProtoMajor: 1, ProtoMinor: 1, Request: dummyReq("GET"), Header: http.Header{ "Foo": []string{" Bar\nBaz "}, }, Body: nil, ContentLength: 0, TransferEncoding: []string{"chunked"}, Close: true, }, "HTTP/1.1 204 No Content\r\n" + "Connection: close\r\n" + "Foo: Bar Baz\r\n" + "\r\n", }, // Want a single Content-Length header. Fixing issue 8180 where // there were two. { Response{ StatusCode: http.StatusOK, ProtoMajor: 1, ProtoMinor: 1, Request: &Request{Method: "POST"}, Header: http.Header{}, ContentLength: 0, TransferEncoding: nil, Body: nil, }, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", }, } for i := range respWriteTests { tt := &respWriteTests[i] var braw bytes.Buffer err := tt.Resp.Write(&braw) if err != nil { t.Errorf("error writing #%d: %s", i, err) continue } sraw := braw.String() if sraw != tt.Raw { t.Errorf("Test %d, expecting:\n%q\nGot:\n%q\n", i, tt.Raw, sraw) continue } } } ubuntu-push-0.68+16.04.20160310.2/http13client/transfer_test.go0000644000015600001650000000275412670364255024221 0ustar pbuserpbgroup00000000000000// Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "bufio" "io" "strings" "testing" ) func TestBodyReadBadTrailer(t *testing.T) { b := &body{ src: strings.NewReader("foobar"), hdr: true, // force reading the trailer r: bufio.NewReader(strings.NewReader("")), } buf := make([]byte, 7) n, err := b.Read(buf[:3]) got := string(buf[:n]) if got != "foo" || err != nil { t.Fatalf(`first Read = %d (%q), %v; want 3 ("foo")`, n, got, err) } n, err = b.Read(buf[:]) got = string(buf[:n]) if got != "bar" || err != nil { t.Fatalf(`second Read = %d (%q), %v; want 3 ("bar")`, n, got, err) } n, err = b.Read(buf[:]) got = string(buf[:n]) if err == nil { t.Errorf("final Read was successful (%q), expected error from trailer read", got) } } func TestFinalChunkedBodyReadEOF(t *testing.T) { res, err := ReadResponse(bufio.NewReader(strings.NewReader( "HTTP/1.1 200 OK\r\n"+ "Transfer-Encoding: chunked\r\n"+ "\r\n"+ "0a\r\n"+ "Body here\n\r\n"+ "09\r\n"+ "continued\r\n"+ "0\r\n"+ "\r\n")), nil) if err != nil { t.Fatal(err) } want := "Body here\ncontinued" buf := make([]byte, len(want)) n, err := res.Body.Read(buf) if n != len(want) || err != io.EOF { t.Logf("body = %#v", res.Body) t.Errorf("Read = %v, %v; want %d, EOF", n, err, len(want)) } if string(buf) != want { t.Errorf("buf = %q; want %q", buf, want) } } ubuntu-push-0.68+16.04.20160310.2/http13client/client_test.go0000644000015600001650000006174212670364270023652 0ustar pbuserpbgroup00000000000000// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Tests for client.go package http_test import ( "bytes" "crypto/tls" "crypto/x509" "encoding/base64" "errors" "fmt" "io" "io/ioutil" . "launchpad.net/ubuntu-push/http13client" "net" "net/http" "net/http/httptest" "net/url" "reflect" "sort" "strconv" "strings" "sync" "testing" "time" ) var robotsTxtHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Last-Modified", "sometime") fmt.Fprintf(w, "User-agent: go\nDisallow: /something/") }) // pedanticReadAll works like ioutil.ReadAll but additionally // verifies that r obeys the documented io.Reader contract. func pedanticReadAll(r io.Reader) (b []byte, err error) { var bufa [64]byte buf := bufa[:] for { n, err := r.Read(buf) if n == 0 && err == nil { return nil, fmt.Errorf("Read: n=0 with err=nil") } b = append(b, buf[:n]...) if err == io.EOF { n, err := r.Read(buf) if n != 0 || err != io.EOF { return nil, fmt.Errorf("Read: n=%d err=%#v after EOF", n, err) } return b, nil } if err != nil { return b, err } } } type chanWriter chan string func (w chanWriter) Write(p []byte) (n int, err error) { w <- string(p) return len(p), nil } func TestClient(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(robotsTxtHandler) defer ts.Close() r, err := Get(ts.URL) var b []byte if err == nil { b, err = pedanticReadAll(r.Body) r.Body.Close() } if err != nil { t.Error(err) } else if s := string(b); !strings.HasPrefix(s, "User-agent:") { t.Errorf("Incorrect page body (did not begin with User-agent): %q", s) } } func TestClientHead(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(robotsTxtHandler) defer ts.Close() r, err := Head(ts.URL) if err != nil { t.Fatal(err) } if _, ok := r.Header["Last-Modified"]; !ok { t.Error("Last-Modified header not found.") } } type recordingTransport struct { req *Request } func (t *recordingTransport) RoundTrip(req *Request) (resp *Response, err error) { t.req = req return nil, errors.New("dummy impl") } func TestGetRequestFormat(t *testing.T) { defer afterTest(t) tr := &recordingTransport{} client := &Client{Transport: tr} url := "http://dummy.faketld/" client.Get(url) // Note: doesn't hit network if tr.req.Method != "GET" { t.Errorf("expected method %q; got %q", "GET", tr.req.Method) } if tr.req.URL.String() != url { t.Errorf("expected URL %q; got %q", url, tr.req.URL.String()) } if tr.req.Header == nil { t.Errorf("expected non-nil request Header") } } func TestPostRequestFormat(t *testing.T) { defer afterTest(t) tr := &recordingTransport{} client := &Client{Transport: tr} url := "http://dummy.faketld/" json := `{"key":"value"}` b := strings.NewReader(json) client.Post(url, "application/json", b) // Note: doesn't hit network if tr.req.Method != "POST" { t.Errorf("got method %q, want %q", tr.req.Method, "POST") } if tr.req.URL.String() != url { t.Errorf("got URL %q, want %q", tr.req.URL.String(), url) } if tr.req.Header == nil { t.Fatalf("expected non-nil request Header") } if tr.req.Close { t.Error("got Close true, want false") } if g, e := tr.req.ContentLength, int64(len(json)); g != e { t.Errorf("got ContentLength %d, want %d", g, e) } } func TestPostFormRequestFormat(t *testing.T) { defer afterTest(t) tr := &recordingTransport{} client := &Client{Transport: tr} urlStr := "http://dummy.faketld/" form := make(url.Values) form.Set("foo", "bar") form.Add("foo", "bar2") form.Set("bar", "baz") client.PostForm(urlStr, form) // Note: doesn't hit network if tr.req.Method != "POST" { t.Errorf("got method %q, want %q", tr.req.Method, "POST") } if tr.req.URL.String() != urlStr { t.Errorf("got URL %q, want %q", tr.req.URL.String(), urlStr) } if tr.req.Header == nil { t.Fatalf("expected non-nil request Header") } if g, e := tr.req.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; g != e { t.Errorf("got Content-Type %q, want %q", g, e) } if tr.req.Close { t.Error("got Close true, want false") } // Depending on map iteration, body can be either of these. expectedBody := "foo=bar&foo=bar2&bar=baz" expectedBody1 := "bar=baz&foo=bar&foo=bar2" if g, e := tr.req.ContentLength, int64(len(expectedBody)); g != e { t.Errorf("got ContentLength %d, want %d", g, e) } bodyb, err := ioutil.ReadAll(tr.req.Body) if err != nil { t.Fatalf("ReadAll on req.Body: %v", err) } if g := string(bodyb); g != expectedBody && g != expectedBody1 { t.Errorf("got body %q, want %q or %q", g, expectedBody, expectedBody1) } } func TestClientRedirects(t *testing.T) { defer afterTest(t) var ts *httptest.Server ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n, _ := strconv.Atoi(r.FormValue("n")) // Test Referer header. (7 is arbitrary position to test at) if n == 7 { if g, e := r.Referer(), ts.URL+"/?n=6"; e != g { t.Errorf("on request ?n=7, expected referer of %q; got %q", e, g) } } if n < 15 { http.Redirect(w, r, fmt.Sprintf("/?n=%d", n+1), http.StatusFound) return } fmt.Fprintf(w, "n=%d", n) })) defer ts.Close() c := &Client{} _, err := c.Get(ts.URL) if e, g := "Get /?n=10: stopped after 10 redirects", fmt.Sprintf("%v", err); e != g { t.Errorf("with default client Get, expected error %q, got %q", e, g) } // HEAD request should also have the ability to follow redirects. _, err = c.Head(ts.URL) if e, g := "Head /?n=10: stopped after 10 redirects", fmt.Sprintf("%v", err); e != g { t.Errorf("with default client Head, expected error %q, got %q", e, g) } // Do should also follow redirects. greq, _ := NewRequest("GET", ts.URL, nil) _, err = c.Do(greq) if e, g := "Get /?n=10: stopped after 10 redirects", fmt.Sprintf("%v", err); e != g { t.Errorf("with default client Do, expected error %q, got %q", e, g) } var checkErr error var lastVia []*Request c = &Client{CheckRedirect: func(_ *Request, via []*Request) error { lastVia = via return checkErr }} res, err := c.Get(ts.URL) if err != nil { t.Fatalf("Get error: %v", err) } res.Body.Close() finalUrl := res.Request.URL.String() if e, g := "", fmt.Sprintf("%v", err); e != g { t.Errorf("with custom client, expected error %q, got %q", e, g) } if !strings.HasSuffix(finalUrl, "/?n=15") { t.Errorf("expected final url to end in /?n=15; got url %q", finalUrl) } if e, g := 15, len(lastVia); e != g { t.Errorf("expected lastVia to have contained %d elements; got %d", e, g) } checkErr = errors.New("no redirects allowed") res, err = c.Get(ts.URL) if urlError, ok := err.(*url.Error); !ok || urlError.Err != checkErr { t.Errorf("with redirects forbidden, expected a *url.Error with our 'no redirects allowed' error inside; got %#v (%q)", err, err) } if res == nil { t.Fatalf("Expected a non-nil Response on CheckRedirect failure (http://golang.org/issue/3795)") } res.Body.Close() if res.Header.Get("Location") == "" { t.Errorf("no Location header in Response") } } func TestPostRedirects(t *testing.T) { defer afterTest(t) var log struct { sync.Mutex bytes.Buffer } var ts *httptest.Server ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Lock() fmt.Fprintf(&log.Buffer, "%s %s ", r.Method, r.RequestURI) log.Unlock() if v := r.URL.Query().Get("code"); v != "" { code, _ := strconv.Atoi(v) if code/100 == 3 { w.Header().Set("Location", ts.URL) } w.WriteHeader(code) } })) defer ts.Close() tests := []struct { suffix string want int // response code }{ {"/", 200}, {"/?code=301", 301}, {"/?code=302", 200}, {"/?code=303", 200}, {"/?code=404", 404}, } for _, tt := range tests { res, err := Post(ts.URL+tt.suffix, "text/plain", strings.NewReader("Some content")) if err != nil { t.Fatal(err) } if res.StatusCode != tt.want { t.Errorf("POST %s: status code = %d; want %d", tt.suffix, res.StatusCode, tt.want) } } log.Lock() got := log.String() log.Unlock() want := "POST / POST /?code=301 POST /?code=302 GET / POST /?code=303 GET / POST /?code=404 " if got != want { t.Errorf("Log differs.\n Got: %q\nWant: %q", got, want) } } var expectedCookies = []*http.Cookie{ {Name: "ChocolateChip", Value: "tasty"}, {Name: "First", Value: "Hit"}, {Name: "Second", Value: "Hit"}, } var echoCookiesRedirectHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for _, cookie := range r.Cookies() { http.SetCookie(w, cookie) } if r.URL.Path == "/" { http.SetCookie(w, expectedCookies[1]) http.Redirect(w, r, "/second", http.StatusMovedPermanently) } else { http.SetCookie(w, expectedCookies[2]) w.Write([]byte("hello")) } }) func TestClientSendsCookieFromJar(t *testing.T) { tr := &recordingTransport{} client := &Client{Transport: tr} client.Jar = &TestJar{perURL: make(map[string][]*http.Cookie)} us := "http://dummy.faketld/" u, _ := url.Parse(us) client.Jar.SetCookies(u, expectedCookies) client.Get(us) // Note: doesn't hit network matchReturnedCookies(t, expectedCookies, tr.req.Cookies()) client.Head(us) // Note: doesn't hit network matchReturnedCookies(t, expectedCookies, tr.req.Cookies()) client.Post(us, "text/plain", strings.NewReader("body")) // Note: doesn't hit network matchReturnedCookies(t, expectedCookies, tr.req.Cookies()) client.PostForm(us, url.Values{}) // Note: doesn't hit network matchReturnedCookies(t, expectedCookies, tr.req.Cookies()) req, _ := NewRequest("GET", us, nil) client.Do(req) // Note: doesn't hit network matchReturnedCookies(t, expectedCookies, tr.req.Cookies()) req, _ = NewRequest("POST", us, nil) client.Do(req) // Note: doesn't hit network matchReturnedCookies(t, expectedCookies, tr.req.Cookies()) } // Just enough correctness for our redirect tests. Uses the URL.Host as the // scope of all cookies. type TestJar struct { m sync.Mutex perURL map[string][]*http.Cookie } func (j *TestJar) SetCookies(u *url.URL, cookies []*http.Cookie) { j.m.Lock() defer j.m.Unlock() if j.perURL == nil { j.perURL = make(map[string][]*http.Cookie) } j.perURL[u.Host] = cookies } func (j *TestJar) Cookies(u *url.URL) []*http.Cookie { j.m.Lock() defer j.m.Unlock() return j.perURL[u.Host] } func TestRedirectCookiesJar(t *testing.T) { defer afterTest(t) var ts *httptest.Server ts = httptest.NewServer(echoCookiesRedirectHandler) defer ts.Close() c := &Client{ Jar: new(TestJar), } u, _ := url.Parse(ts.URL) c.Jar.SetCookies(u, []*http.Cookie{expectedCookies[0]}) resp, err := c.Get(ts.URL) if err != nil { t.Fatalf("Get: %v", err) } resp.Body.Close() matchReturnedCookies(t, expectedCookies, resp.Cookies()) } func matchReturnedCookies(t *testing.T, expected, given []*http.Cookie) { if len(given) != len(expected) { t.Logf("Received cookies: %v", given) t.Errorf("Expected %d cookies, got %d", len(expected), len(given)) } for _, ec := range expected { foundC := false for _, c := range given { if ec.Name == c.Name && ec.Value == c.Value { foundC = true break } } if !foundC { t.Errorf("Missing cookie %v", ec) } } } func TestJarCalls(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { pathSuffix := r.RequestURI[1:] if r.RequestURI == "/nosetcookie" { return // dont set cookies for this path } http.SetCookie(w, &http.Cookie{Name: "name" + pathSuffix, Value: "val" + pathSuffix}) if r.RequestURI == "/" { http.Redirect(w, r, "http://secondhost.fake/secondpath", 302) } })) defer ts.Close() jar := new(RecordingJar) c := &Client{ Jar: jar, Transport: &Transport{ Dial: func(_ string, _ string) (net.Conn, error) { return net.Dial("tcp", ts.Listener.Addr().String()) }, }, } _, err := c.Get("http://firsthost.fake/") if err != nil { t.Fatal(err) } _, err = c.Get("http://firsthost.fake/nosetcookie") if err != nil { t.Fatal(err) } got := jar.log.String() want := `Cookies("http://firsthost.fake/") SetCookie("http://firsthost.fake/", [name=val]) Cookies("http://secondhost.fake/secondpath") SetCookie("http://secondhost.fake/secondpath", [namesecondpath=valsecondpath]) Cookies("http://firsthost.fake/nosetcookie") ` if got != want { t.Errorf("Got Jar calls:\n%s\nWant:\n%s", got, want) } } // RecordingJar keeps a log of calls made to it, without // tracking any cookies. type RecordingJar struct { mu sync.Mutex log bytes.Buffer } func (j *RecordingJar) SetCookies(u *url.URL, cookies []*http.Cookie) { j.logf("SetCookie(%q, %v)\n", u, cookies) } func (j *RecordingJar) Cookies(u *url.URL) []*http.Cookie { j.logf("Cookies(%q)\n", u) return nil } func (j *RecordingJar) logf(format string, args ...interface{}) { j.mu.Lock() defer j.mu.Unlock() fmt.Fprintf(&j.log, format, args...) } func TestStreamingGet(t *testing.T) { defer afterTest(t) say := make(chan string) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.(http.Flusher).Flush() for str := range say { w.Write([]byte(str)) w.(http.Flusher).Flush() } })) defer ts.Close() c := &Client{} res, err := c.Get(ts.URL) if err != nil { t.Fatal(err) } var buf [10]byte for _, str := range []string{"i", "am", "also", "known", "as", "comet"} { say <- str n, err := io.ReadFull(res.Body, buf[0:len(str)]) if err != nil { t.Fatalf("ReadFull on %q: %v", str, err) } if n != len(str) { t.Fatalf("Receiving %q, only read %d bytes", str, n) } got := string(buf[0:n]) if got != str { t.Fatalf("Expected %q, got %q", str, got) } } close(say) _, err = io.ReadFull(res.Body, buf[0:1]) if err != io.EOF { t.Fatalf("at end expected EOF, got %v", err) } } type writeCountingConn struct { net.Conn count *int } func (c *writeCountingConn) Write(p []byte) (int, error) { *c.count++ return c.Conn.Write(p) } // TestClientWrites verifies that client requests are buffered and we // don't send a TCP packet per line of the http request + body. func TestClientWrites(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { })) defer ts.Close() writes := 0 dialer := func(netz string, addr string) (net.Conn, error) { c, err := net.Dial(netz, addr) if err == nil { c = &writeCountingConn{c, &writes} } return c, err } c := &Client{Transport: &Transport{Dial: dialer}} _, err := c.Get(ts.URL) if err != nil { t.Fatal(err) } if writes != 1 { t.Errorf("Get request did %d Write calls, want 1", writes) } writes = 0 _, err = c.PostForm(ts.URL, url.Values{"foo": {"bar"}}) if err != nil { t.Fatal(err) } if writes != 1 { t.Errorf("Post request did %d Write calls, want 1", writes) } } func TestClientErrorWithRequestURI(t *testing.T) { defer afterTest(t) req, _ := NewRequest("GET", "http://localhost:1234/", nil) req.RequestURI = "/this/field/is/illegal/and/should/error/" _, err := DefaultClient.Do(req) if err == nil { t.Fatalf("expected an error") } if !strings.Contains(err.Error(), "RequestURI") { t.Errorf("wanted error mentioning RequestURI; got error: %v", err) } } func newTLSTransport(t *testing.T, ts *httptest.Server) *Transport { certs := x509.NewCertPool() for _, c := range ts.TLS.Certificates { roots, err := x509.ParseCertificates(c.Certificate[len(c.Certificate)-1]) if err != nil { t.Fatalf("error parsing server's root cert: %v", err) } for _, root := range roots { certs.AddCert(root) } } return &Transport{ TLSClientConfig: &tls.Config{RootCAs: certs}, } } func TestClientWithCorrectTLSServerName(t *testing.T) { defer afterTest(t) const serverName = "example.com" ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.TLS.ServerName != serverName { t.Errorf("expected client to set ServerName %q, got: %q", serverName, r.TLS.ServerName) } })) defer ts.Close() trans := newTLSTransport(t, ts) trans.TLSClientConfig.ServerName = serverName c := &Client{Transport: trans} if _, err := c.Get(ts.URL); err != nil { t.Fatalf("expected successful TLS connection, got error: %v", err) } } // Test for golang.org/issue/5829; the Transport should respect TLSClientConfig.ServerName // when not empty. // // tls.Config.ServerName (non-empty, set to "example.com") takes // precedence over "some-other-host.tld" which previously incorrectly // took precedence. We don't actually connect to (or even resolve) // "some-other-host.tld", though, because of the Transport.Dial hook. // // The httptest.Server has a cert with "example.com" as its name. func TestTransportUsesTLSConfigServerName(t *testing.T) { defer afterTest(t) ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello")) })) defer ts.Close() tr := newTLSTransport(t, ts) tr.TLSClientConfig.ServerName = "example.com" // one of httptest's Server cert names tr.Dial = func(netw, addr string) (net.Conn, error) { return net.Dial(netw, ts.Listener.Addr().String()) } defer tr.CloseIdleConnections() c := &Client{Transport: tr} res, err := c.Get("https://some-other-host.tld/") if err != nil { t.Fatal(err) } res.Body.Close() } func TestResponseSetsTLSConnectionState(t *testing.T) { defer afterTest(t) ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello")) })) defer ts.Close() tr := newTLSTransport(t, ts) tr.TLSClientConfig.CipherSuites = []uint16{tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA} tr.Dial = func(netw, addr string) (net.Conn, error) { return net.Dial(netw, ts.Listener.Addr().String()) } defer tr.CloseIdleConnections() c := &Client{Transport: tr} res, err := c.Get("https://example.com/") if err != nil { t.Fatal(err) } defer res.Body.Close() if res.TLS == nil { t.Fatal("Response didn't set TLS Connection State.") } if got, want := res.TLS.CipherSuite, tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA; got != want { t.Errorf("TLS Cipher Suite = %d; want %d", got, want) } } // Verify Response.ContentLength is populated. http://golang.org/issue/4126 func TestClientHeadContentLength(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if v := r.FormValue("cl"); v != "" { w.Header().Set("Content-Length", v) } })) defer ts.Close() tests := []struct { suffix string want int64 }{ {"/?cl=1234", 1234}, {"/?cl=0", 0}, {"", -1}, } for _, tt := range tests { req, _ := NewRequest("HEAD", ts.URL+tt.suffix, nil) res, err := DefaultClient.Do(req) if err != nil { t.Fatal(err) } if res.ContentLength != tt.want { t.Errorf("Content-Length = %d; want %d", res.ContentLength, tt.want) } bs, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatal(err) } if len(bs) != 0 { t.Errorf("Unexpected content: %q", bs) } } } func TestEmptyPasswordAuth(t *testing.T) { defer afterTest(t) gopher := "gopher" ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if strings.HasPrefix(auth, "Basic ") { encoded := auth[6:] decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { t.Fatal(err) } expected := gopher + ":" s := string(decoded) if expected != s { t.Errorf("Invalid Authorization header. Got %q, wanted %q", s, expected) } } else { t.Errorf("Invalid auth %q", auth) } })) defer ts.Close() c := &Client{} req, err := NewRequest("GET", ts.URL, nil) if err != nil { t.Fatal(err) } req.URL.User = url.User(gopher) resp, err := c.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() } func TestBasicAuth(t *testing.T) { defer afterTest(t) tr := &recordingTransport{} client := &Client{Transport: tr} url := "http://My%20User:My%20Pass@dummy.faketld/" expected := "My User:My Pass" client.Get(url) if tr.req.Method != "GET" { t.Errorf("got method %q, want %q", tr.req.Method, "GET") } if tr.req.URL.String() != url { t.Errorf("got URL %q, want %q", tr.req.URL.String(), url) } if tr.req.Header == nil { t.Fatalf("expected non-nil request Header") } auth := tr.req.Header.Get("Authorization") if strings.HasPrefix(auth, "Basic ") { encoded := auth[6:] decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { t.Fatal(err) } s := string(decoded) if expected != s { t.Errorf("Invalid Authorization header. Got %q, wanted %q", s, expected) } } else { t.Errorf("Invalid auth %q", auth) } } func TestClientTimeout(t *testing.T) { if testing.Short() { t.Skip("skipping in short mode") } defer afterTest(t) sawRoot := make(chan bool, 1) sawSlow := make(chan bool, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { sawRoot <- true http.Redirect(w, r, "/slow", http.StatusFound) return } if r.URL.Path == "/slow" { w.Write([]byte("Hello")) w.(http.Flusher).Flush() sawSlow <- true time.Sleep(2 * time.Second) return } })) defer ts.Close() const timeout = 500 * time.Millisecond c := &Client{ Timeout: timeout, } res, err := c.Get(ts.URL) if err != nil { t.Fatal(err) } select { case <-sawRoot: // good. default: t.Fatal("handler never got / request") } select { case <-sawSlow: // good. default: t.Fatal("handler never got /slow request") } errc := make(chan error, 1) go func() { _, err := ioutil.ReadAll(res.Body) errc <- err res.Body.Close() }() const failTime = timeout * 2 select { case err := <-errc: if err == nil { t.Error("expected error from ReadAll") } // Expected error. case <-time.After(failTime): t.Errorf("timeout after %v waiting for timeout of %v", failTime, timeout) } } func TestClientRedirectEatsBody(t *testing.T) { defer afterTest(t) saw := make(chan string, 2) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { saw <- r.RemoteAddr if r.URL.Path == "/" { http.Redirect(w, r, "/foo", http.StatusFound) // which includes a body } })) defer ts.Close() res, err := Get(ts.URL) if err != nil { t.Fatal(err) } _, err = ioutil.ReadAll(res.Body) if err != nil { t.Fatal(err) } res.Body.Close() var first string select { case first = <-saw: default: t.Fatal("server didn't see a request") } var second string select { case second = <-saw: default: t.Fatal("server didn't see a second request") } if first != second { t.Fatal("server saw different client ports before & after the redirect") } } // eofReaderFunc is an io.Reader that runs itself, and then returns io.EOF. type eofReaderFunc func() func (f eofReaderFunc) Read(p []byte) (n int, err error) { f() return 0, io.EOF } func TestClientTrailers(t *testing.T) { defer afterTest(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Connection", "close") w.Header().Set("Trailer", "Server-Trailer-A, Server-Trailer-B") w.Header().Add("Trailer", "Server-Trailer-C") var decl []string for k := range r.Trailer { decl = append(decl, k) } sort.Strings(decl) slurp, err := ioutil.ReadAll(r.Body) if err != nil { t.Errorf("Server reading request body: %v", err) } if string(slurp) != "foo" { t.Errorf("Server read request body %q; want foo", slurp) } if r.Trailer == nil { io.WriteString(w, "nil Trailer") } else { fmt.Fprintf(w, "decl: %v, vals: %s, %s", decl, r.Trailer.Get("Client-Trailer-A"), r.Trailer.Get("Client-Trailer-B")) } // TODO: golang.org/issue/7759: there's no way yet for // the server to set trailers without hijacking, so do // that for now, just to test the client. Later, in // Go 1.4, it should be implicit that any mutations // to w.Header() after the initial write are the // trailers to be sent, if and only if they were // previously declared with w.Header().Set("Trailer", // ..keys..) w.(http.Flusher).Flush() conn, buf, _ := w.(http.Hijacker).Hijack() t := http.Header{} t.Set("Server-Trailer-A", "valuea") t.Set("Server-Trailer-C", "valuec") // skipping B buf.WriteString("0\r\n") // eof t.Write(buf) buf.WriteString("\r\n") // end of trailers buf.Flush() conn.Close() })) defer ts.Close() var req *Request req, _ = NewRequest("POST", ts.URL, io.MultiReader( eofReaderFunc(func() { req.Trailer["Client-Trailer-A"] = []string{"valuea"} }), strings.NewReader("foo"), eofReaderFunc(func() { req.Trailer["Client-Trailer-B"] = []string{"valueb"} }), )) req.Trailer = http.Header{ "Client-Trailer-A": nil, // to be set later "Client-Trailer-B": nil, // to be set later } req.ContentLength = -1 res, err := DefaultClient.Do(req) if err != nil { t.Fatal(err) } if err := wantBody(res, err, "decl: [Client-Trailer-A Client-Trailer-B], vals: valuea, valueb"); err != nil { t.Error(err) } want := http.Header{ "Server-Trailer-A": []string{"valuea"}, "Server-Trailer-B": nil, "Server-Trailer-C": []string{"valuec"}, } if !reflect.DeepEqual(res.Trailer, want) { t.Errorf("Response trailers = %#v; want %#v", res.Trailer, want) } } ubuntu-push-0.68+16.04.20160310.2/http13client/serve_test.go0000644000015600001650000000230612670364255023512 0ustar pbuserpbgroup00000000000000// Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http_test import ( "io" "net" "time" ) type dummyAddr string func (a dummyAddr) Network() string { return string(a) } func (a dummyAddr) String() string { return string(a) } type noopConn struct{} func (noopConn) LocalAddr() net.Addr { return dummyAddr("local-addr") } func (noopConn) RemoteAddr() net.Addr { return dummyAddr("remote-addr") } func (noopConn) SetDeadline(t time.Time) error { return nil } func (noopConn) SetReadDeadline(t time.Time) error { return nil } func (noopConn) SetWriteDeadline(t time.Time) error { return nil } type rwTestConn struct { io.Reader io.Writer noopConn closeFunc func() error // called if non-nil closec chan bool // else, if non-nil, send value to it on close } func (c *rwTestConn) Close() error { if c.closeFunc != nil { return c.closeFunc() } select { case c.closec <- true: default: } return nil } type neverEnding byte func (b neverEnding) Read(p []byte) (n int, err error) { for i := range p { p[i] = byte(b) } return len(p), nil } ubuntu-push-0.68+16.04.20160310.2/http13client/Makefile0000644000015600001650000000244612670364255022445 0ustar pbuserpbgroup00000000000000# help massage and extract from go development 1.3 net/http the client bits grab: cp $(GOCHECKOUT)/src/pkg/net/http/*.go . cp $(GOCHECKOUT)/LICENSE . mkdir -p httptest mkdir -p httputil mkdir -p testdata cp $(GOCHECKOUT)/src/pkg/net/http/httptest/*.go httptest cp $(GOCHECKOUT)/src/pkg/net//http/httputil/*.go httputil cp $(GOCHECKOUT)/src/pkg/net/http/testdata/* testdata hg -R $(GOCHECKOUT) summary > _using.txt full-prepare: patch -R -p5 < _patches/sync_pool.Rpatch patch -p1 < _patches/no_keepalive.patch sed -i -e 's+"net/http"+"launchpad.net/ubuntu-push/http13client"+' *.go httptest/*.go httputil/*.go sed -i -e 's+"net/http/+"launchpad.net/ubuntu-push/http13client/+' *.go httptest/*.go httputil/*.go patch -p1 < _patches/no_serve_test_unsupported_bench.patch prune: rm -rf example_test.go filetransport*.go fs*.go race.go range_test.go \ sniff*.go httptest httputil testdata triv.go jar.go status.go \ cookie_test.go sed -i -e 's+"launchpad.net/ubuntu-push/http13client/+"net/http/+' *.go fix: patch -p1 < _patches/empty_server.patch patch -p1 < _patches/fix_tests.patch patch -p1 < _patches/fix_code.patch patch -p1 < _patches/fix_status.patch patch -p1 < _patches/tweak_doc_go.patch go fmt wipe: rm -rf *.go httptest httputil testdata .PHONY: grab full-prepare prune fix wipe ubuntu-push-0.68+16.04.20160310.2/http13client/transport.go0000644000015600001650000007740712670364255023401 0ustar pbuserpbgroup00000000000000// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // HTTP client implementation. See RFC 2616. // // This is the low-level Transport implementation of RoundTripper. // The high-level interface is in client.go. package http import ( "bufio" "compress/gzip" "crypto/tls" "errors" "fmt" "io" "log" "net" "net/http" "net/url" "os" "strings" "sync" "time" ) // DefaultTransport is the default implementation of Transport and is // used by DefaultClient. It establishes network connections as needed // and caches them for reuse by subsequent calls. It uses HTTP proxies // as directed by the $HTTP_PROXY and $NO_PROXY (or $http_proxy and // $no_proxy) environment variables. var DefaultTransport RoundTripper = &Transport{ Proxy: ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, // KeepAlive: 30 * time.Second, }).Dial, TLSHandshakeTimeout: 10 * time.Second, } // DefaultMaxIdleConnsPerHost is the default value of Transport's // MaxIdleConnsPerHost. const DefaultMaxIdleConnsPerHost = 2 // Transport is an implementation of RoundTripper that supports http, // https, and http proxies (for either http or https with CONNECT). // Transport can also cache connections for future re-use. type Transport struct { idleMu sync.Mutex idleConn map[connectMethodKey][]*persistConn idleConnCh map[connectMethodKey]chan *persistConn reqMu sync.Mutex reqCanceler map[*Request]func() altMu sync.RWMutex altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper // Proxy specifies a function to return a proxy for a given // Request. If the function returns a non-nil error, the // request is aborted with the provided error. // If Proxy is nil or returns a nil *URL, no proxy is used. Proxy func(*Request) (*url.URL, error) // Dial specifies the dial function for creating TCP // connections. // If Dial is nil, net.Dial is used. Dial func(network, addr string) (net.Conn, error) // TLSClientConfig specifies the TLS configuration to use with // tls.Client. If nil, the default configuration is used. TLSClientConfig *tls.Config // TLSHandshakeTimeout specifies the maximum amount of time waiting to // wait for a TLS handshake. Zero means no timeout. TLSHandshakeTimeout time.Duration // DisableKeepAlives, if true, prevents re-use of TCP connections // between different HTTP requests. DisableKeepAlives bool // DisableCompression, if true, prevents the Transport from // requesting compression with an "Accept-Encoding: gzip" // request header when the Request contains no existing // Accept-Encoding value. If the Transport requests gzip on // its own and gets a gzipped response, it's transparently // decoded in the Response.Body. However, if the user // explicitly requested gzip it is not automatically // uncompressed. DisableCompression bool // MaxIdleConnsPerHost, if non-zero, controls the maximum idle // (keep-alive) to keep per-host. If zero, // DefaultMaxIdleConnsPerHost is used. MaxIdleConnsPerHost int // ResponseHeaderTimeout, if non-zero, specifies the amount of // time to wait for a server's response headers after fully // writing the request (including its body, if any). This // time does not include the time to read the response body. ResponseHeaderTimeout time.Duration // TODO: tunable on global max cached connections // TODO: tunable on timeout on cached connections } // ProxyFromEnvironment returns the URL of the proxy to use for a // given request, as indicated by the environment variables // $HTTP_PROXY and $NO_PROXY (or $http_proxy and $no_proxy). // An error is returned if the proxy environment is invalid. // A nil URL and nil error are returned if no proxy is defined in the // environment, or a proxy should not be used for the given request. // // As a special case, if req.URL.Host is "localhost" (with or without // a port number), then a nil URL and nil error will be returned. func ProxyFromEnvironment(req *Request) (*url.URL, error) { proxy := httpProxyEnv.Get() if proxy == "" { return nil, nil } if !useProxy(canonicalAddr(req.URL)) { return nil, nil } proxyURL, err := url.Parse(proxy) if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") { // proxy was bogus. Try prepending "http://" to it and // see if that parses correctly. If not, we fall // through and complain about the original one. if proxyURL, err := url.Parse("http://" + proxy); err == nil { return proxyURL, nil } } if err != nil { return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err) } return proxyURL, nil } // ProxyURL returns a proxy function (for use in a Transport) // that always returns the same URL. func ProxyURL(fixedURL *url.URL) func(*Request) (*url.URL, error) { return func(*Request) (*url.URL, error) { return fixedURL, nil } } // transportRequest is a wrapper around a *Request that adds // optional extra headers to write. type transportRequest struct { *Request // original request, not to be mutated extra http.Header // extra headers to write, or nil } func (tr *transportRequest) extraHeaders() http.Header { if tr.extra == nil { tr.extra = make(http.Header) } return tr.extra } // RoundTrip implements the RoundTripper interface. // // For higher-level HTTP client support (such as handling of cookies // and redirects), see Get, Post, and the Client type. func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) { if req.URL == nil { req.closeBody() return nil, errors.New("http: nil Request.URL") } if req.Header == nil { req.closeBody() return nil, errors.New("http: nil Request.Header") } if req.URL.Scheme != "http" && req.URL.Scheme != "https" { t.altMu.RLock() var rt RoundTripper if t.altProto != nil { rt = t.altProto[req.URL.Scheme] } t.altMu.RUnlock() if rt == nil { req.closeBody() return nil, &badStringError{"unsupported protocol scheme", req.URL.Scheme} } return rt.RoundTrip(req) } if req.URL.Host == "" { req.closeBody() return nil, errors.New("http: no Host in request URL") } treq := &transportRequest{Request: req} cm, err := t.connectMethodForRequest(treq) if err != nil { req.closeBody() return nil, err } // Get the cached or newly-created connection to either the // host (for http or https), the http proxy, or the http proxy // pre-CONNECTed to https server. In any case, we'll be ready // to send it requests. pconn, err := t.getConn(req, cm) if err != nil { t.setReqCanceler(req, nil) req.closeBody() return nil, err } return pconn.roundTrip(treq) } // RegisterProtocol registers a new protocol with scheme. // The Transport will pass requests using the given scheme to rt. // It is rt's responsibility to simulate HTTP request semantics. // // RegisterProtocol can be used by other packages to provide // implementations of protocol schemes like "ftp" or "file". func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) { if scheme == "http" || scheme == "https" { panic("protocol " + scheme + " already registered") } t.altMu.Lock() defer t.altMu.Unlock() if t.altProto == nil { t.altProto = make(map[string]RoundTripper) } if _, exists := t.altProto[scheme]; exists { panic("protocol " + scheme + " already registered") } t.altProto[scheme] = rt } // CloseIdleConnections closes any connections which were previously // connected from previous requests but are now sitting idle in // a "keep-alive" state. It does not interrupt any connections currently // in use. func (t *Transport) CloseIdleConnections() { t.idleMu.Lock() m := t.idleConn t.idleConn = nil t.idleConnCh = nil t.idleMu.Unlock() for _, conns := range m { for _, pconn := range conns { pconn.close() } } } // CancelRequest cancels an in-flight request by closing its // connection. func (t *Transport) CancelRequest(req *Request) { t.reqMu.Lock() cancel := t.reqCanceler[req] t.reqMu.Unlock() if cancel != nil { cancel() } } // // Private implementation past this point. // var ( httpProxyEnv = &envOnce{ names: []string{"HTTP_PROXY", "http_proxy"}, } noProxyEnv = &envOnce{ names: []string{"NO_PROXY", "no_proxy"}, } ) // envOnce looks up an environment variable (optionally by multiple // names) once. It mitigates expensive lookups on some platforms // (e.g. Windows). type envOnce struct { names []string once sync.Once val string } func (e *envOnce) Get() string { e.once.Do(e.init) return e.val } func (e *envOnce) init() { for _, n := range e.names { e.val = os.Getenv(n) if e.val != "" { return } } } // reset is used by tests func (e *envOnce) reset() { e.once = sync.Once{} e.val = "" } func (t *Transport) connectMethodForRequest(treq *transportRequest) (cm connectMethod, err error) { cm.targetScheme = treq.URL.Scheme cm.targetAddr = canonicalAddr(treq.URL) if t.Proxy != nil { cm.proxyURL, err = t.Proxy(treq.Request) } return cm, nil } // proxyAuth returns the Proxy-Authorization header to set // on requests, if applicable. func (cm *connectMethod) proxyAuth() string { if cm.proxyURL == nil { return "" } if u := cm.proxyURL.User; u != nil { username := u.Username() password, _ := u.Password() return "Basic " + basicAuth(username, password) } return "" } // putIdleConn adds pconn to the list of idle persistent connections awaiting // a new request. // If pconn is no longer needed or not in a good state, putIdleConn // returns false. func (t *Transport) putIdleConn(pconn *persistConn) bool { if t.DisableKeepAlives || t.MaxIdleConnsPerHost < 0 { pconn.close() return false } if pconn.isBroken() { return false } key := pconn.cacheKey max := t.MaxIdleConnsPerHost if max == 0 { max = DefaultMaxIdleConnsPerHost } t.idleMu.Lock() waitingDialer := t.idleConnCh[key] select { case waitingDialer <- pconn: // We're done with this pconn and somebody else is // currently waiting for a conn of this type (they're // actively dialing, but this conn is ready // first). Chrome calls this socket late binding. See // https://insouciant.org/tech/connection-management-in-chromium/ t.idleMu.Unlock() return true default: if waitingDialer != nil { // They had populated this, but their dial won // first, so we can clean up this map entry. delete(t.idleConnCh, key) } } if t.idleConn == nil { t.idleConn = make(map[connectMethodKey][]*persistConn) } if len(t.idleConn[key]) >= max { t.idleMu.Unlock() pconn.close() return false } for _, exist := range t.idleConn[key] { if exist == pconn { log.Fatalf("dup idle pconn %p in freelist", pconn) } } t.idleConn[key] = append(t.idleConn[key], pconn) t.idleMu.Unlock() return true } // getIdleConnCh returns a channel to receive and return idle // persistent connection for the given connectMethod. // It may return nil, if persistent connections are not being used. func (t *Transport) getIdleConnCh(cm connectMethod) chan *persistConn { if t.DisableKeepAlives { return nil } key := cm.key() t.idleMu.Lock() defer t.idleMu.Unlock() if t.idleConnCh == nil { t.idleConnCh = make(map[connectMethodKey]chan *persistConn) } ch, ok := t.idleConnCh[key] if !ok { ch = make(chan *persistConn) t.idleConnCh[key] = ch } return ch } func (t *Transport) getIdleConn(cm connectMethod) (pconn *persistConn) { key := cm.key() t.idleMu.Lock() defer t.idleMu.Unlock() if t.idleConn == nil { return nil } for { pconns, ok := t.idleConn[key] if !ok { return nil } if len(pconns) == 1 { pconn = pconns[0] delete(t.idleConn, key) } else { // 2 or more cached connections; pop last // TODO: queue? pconn = pconns[len(pconns)-1] t.idleConn[key] = pconns[:len(pconns)-1] } if !pconn.isBroken() { return } } } func (t *Transport) setReqCanceler(r *Request, fn func()) { t.reqMu.Lock() defer t.reqMu.Unlock() if t.reqCanceler == nil { t.reqCanceler = make(map[*Request]func()) } if fn != nil { t.reqCanceler[r] = fn } else { delete(t.reqCanceler, r) } } func (t *Transport) dial(network, addr string) (c net.Conn, err error) { if t.Dial != nil { return t.Dial(network, addr) } return net.Dial(network, addr) } // getConn dials and creates a new persistConn to the target as // specified in the connectMethod. This includes doing a proxy CONNECT // and/or setting up TLS. If this doesn't return an error, the persistConn // is ready to write requests to. func (t *Transport) getConn(req *Request, cm connectMethod) (*persistConn, error) { if pc := t.getIdleConn(cm); pc != nil { return pc, nil } type dialRes struct { pc *persistConn err error } dialc := make(chan dialRes) handlePendingDial := func() { if v := <-dialc; v.err == nil { t.putIdleConn(v.pc) } } cancelc := make(chan struct{}) t.setReqCanceler(req, func() { close(cancelc) }) go func() { pc, err := t.dialConn(cm) dialc <- dialRes{pc, err} }() idleConnCh := t.getIdleConnCh(cm) select { case v := <-dialc: // Our dial finished. return v.pc, v.err case pc := <-idleConnCh: // Another request finished first and its net.Conn // became available before our dial. Or somebody // else's dial that they didn't use. // But our dial is still going, so give it away // when it finishes: go handlePendingDial() return pc, nil case <-cancelc: go handlePendingDial() return nil, errors.New("net/http: request canceled while waiting for connection") } } func (t *Transport) dialConn(cm connectMethod) (*persistConn, error) { conn, err := t.dial("tcp", cm.addr()) if err != nil { if cm.proxyURL != nil { err = fmt.Errorf("http: error connecting to proxy %s: %v", cm.proxyURL, err) } return nil, err } pa := cm.proxyAuth() pconn := &persistConn{ t: t, cacheKey: cm.key(), conn: conn, reqch: make(chan requestAndChan, 1), writech: make(chan writeRequest, 1), closech: make(chan struct{}), writeErrCh: make(chan error, 1), } switch { case cm.proxyURL == nil: // Do nothing. case cm.targetScheme == "http": pconn.isProxy = true if pa != "" { pconn.mutateHeaderFunc = func(h http.Header) { h.Set("Proxy-Authorization", pa) } } case cm.targetScheme == "https": connectReq := &Request{ Method: "CONNECT", URL: &url.URL{Opaque: cm.targetAddr}, Host: cm.targetAddr, Header: make(http.Header), } if pa != "" { connectReq.Header.Set("Proxy-Authorization", pa) } connectReq.Write(conn) // Read response. // Okay to use and discard buffered reader here, because // TLS server will not speak until spoken to. br := bufio.NewReader(conn) resp, err := ReadResponse(br, connectReq) if err != nil { conn.Close() return nil, err } if resp.StatusCode != 200 { f := strings.SplitN(resp.Status, " ", 2) conn.Close() return nil, errors.New(f[1]) } } if cm.targetScheme == "https" { // Initiate TLS and check remote host name against certificate. cfg := t.TLSClientConfig if cfg == nil || cfg.ServerName == "" { host := cm.tlsHost() if cfg == nil { cfg = &tls.Config{ServerName: host} } else { clone := *cfg // shallow clone clone.ServerName = host cfg = &clone } } plainConn := conn tlsConn := tls.Client(plainConn, cfg) errc := make(chan error, 2) var timer *time.Timer // for canceling TLS handshake if d := t.TLSHandshakeTimeout; d != 0 { timer = time.AfterFunc(d, func() { errc <- tlsHandshakeTimeoutError{} }) } go func() { err := tlsConn.Handshake() if timer != nil { timer.Stop() } errc <- err }() if err := <-errc; err != nil { plainConn.Close() return nil, err } if !cfg.InsecureSkipVerify { if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { plainConn.Close() return nil, err } } cs := tlsConn.ConnectionState() pconn.tlsState = &cs pconn.conn = tlsConn } pconn.br = bufio.NewReader(noteEOFReader{pconn.conn, &pconn.sawEOF}) pconn.bw = bufio.NewWriter(pconn.conn) go pconn.readLoop() go pconn.writeLoop() return pconn, nil } // useProxy returns true if requests to addr should use a proxy, // according to the NO_PROXY or no_proxy environment variable. // addr is always a canonicalAddr with a host and port. func useProxy(addr string) bool { if len(addr) == 0 { return true } host, _, err := net.SplitHostPort(addr) if err != nil { return false } if host == "localhost" { return false } if ip := net.ParseIP(host); ip != nil { if ip.IsLoopback() { return false } } no_proxy := noProxyEnv.Get() if no_proxy == "*" { return false } addr = strings.ToLower(strings.TrimSpace(addr)) if hasPort(addr) { addr = addr[:strings.LastIndex(addr, ":")] } for _, p := range strings.Split(no_proxy, ",") { p = strings.ToLower(strings.TrimSpace(p)) if len(p) == 0 { continue } if hasPort(p) { p = p[:strings.LastIndex(p, ":")] } if addr == p { return false } if p[0] == '.' && (strings.HasSuffix(addr, p) || addr == p[1:]) { // no_proxy ".foo.com" matches "bar.foo.com" or "foo.com" return false } if p[0] != '.' && strings.HasSuffix(addr, p) && addr[len(addr)-len(p)-1] == '.' { // no_proxy "foo.com" matches "bar.foo.com" return false } } return true } // connectMethod is the map key (in its String form) for keeping persistent // TCP connections alive for subsequent HTTP requests. // // A connect method may be of the following types: // // Cache key form Description // ----------------- ------------------------- // |http|foo.com http directly to server, no proxy // |https|foo.com https directly to server, no proxy // http://proxy.com|https|foo.com http to proxy, then CONNECT to foo.com // http://proxy.com|http http to proxy, http to anywhere after that // // Note: no support to https to the proxy yet. // type connectMethod struct { proxyURL *url.URL // nil for no proxy, else full proxy URL targetScheme string // "http" or "https" targetAddr string // Not used if proxy + http targetScheme (4th example in table) } func (cm *connectMethod) key() connectMethodKey { proxyStr := "" targetAddr := cm.targetAddr if cm.proxyURL != nil { proxyStr = cm.proxyURL.String() if cm.targetScheme == "http" { targetAddr = "" } } return connectMethodKey{ proxy: proxyStr, scheme: cm.targetScheme, addr: targetAddr, } } // addr returns the first hop "host:port" to which we need to TCP connect. func (cm *connectMethod) addr() string { if cm.proxyURL != nil { return canonicalAddr(cm.proxyURL) } return cm.targetAddr } // tlsHost returns the host name to match against the peer's // TLS certificate. func (cm *connectMethod) tlsHost() string { h := cm.targetAddr if hasPort(h) { h = h[:strings.LastIndex(h, ":")] } return h } // connectMethodKey is the map key version of connectMethod, with a // stringified proxy URL (or the empty string) instead of a pointer to // a URL. type connectMethodKey struct { proxy, scheme, addr string } func (k connectMethodKey) String() string { // Only used by tests. return fmt.Sprintf("%s|%s|%s", k.proxy, k.scheme, k.addr) } // persistConn wraps a connection, usually a persistent one // (but may be used for non-keep-alive requests as well) type persistConn struct { t *Transport cacheKey connectMethodKey conn net.Conn tlsState *tls.ConnectionState br *bufio.Reader // from conn sawEOF bool // whether we've seen EOF from conn; owned by readLoop bw *bufio.Writer // to conn reqch chan requestAndChan // written by roundTrip; read by readLoop writech chan writeRequest // written by roundTrip; read by writeLoop closech chan struct{} // closed when conn closed isProxy bool // writeErrCh passes the request write error (usually nil) // from the writeLoop goroutine to the readLoop which passes // it off to the res.Body reader, which then uses it to decide // whether or not a connection can be reused. Issue 7569. writeErrCh chan error lk sync.Mutex // guards following fields numExpectedResponses int closed bool // whether conn has been closed broken bool // an error has happened on this connection; marked broken so it's not reused. // mutateHeaderFunc is an optional func to modify extra // headers on each outbound request before it's written. (the // original Request given to RoundTrip is not modified) mutateHeaderFunc func(http.Header) } // isBroken reports whether this connection is in a known broken state. func (pc *persistConn) isBroken() bool { pc.lk.Lock() b := pc.broken pc.lk.Unlock() return b } func (pc *persistConn) cancelRequest() { pc.conn.Close() } var remoteSideClosedFunc func(error) bool // or nil to use default func remoteSideClosed(err error) bool { if err == io.EOF { return true } if remoteSideClosedFunc != nil { return remoteSideClosedFunc(err) } return false } func (pc *persistConn) readLoop() { alive := true for alive { pb, err := pc.br.Peek(1) pc.lk.Lock() if pc.numExpectedResponses == 0 { if !pc.closed { pc.closeLocked() if len(pb) > 0 { log.Printf("Unsolicited response received on idle HTTP channel starting with %q; err=%v", string(pb), err) } } pc.lk.Unlock() return } pc.lk.Unlock() rc := <-pc.reqch var resp *Response if err == nil { resp, err = ReadResponse(pc.br, rc.req) if err == nil && resp.StatusCode == 100 { // Skip any 100-continue for now. // TODO(bradfitz): if rc.req had "Expect: 100-continue", // actually block the request body write and signal the // writeLoop now to begin sending it. (Issue 2184) For now we // eat it, since we're never expecting one. resp, err = ReadResponse(pc.br, rc.req) } } if resp != nil { resp.TLS = pc.tlsState } hasBody := resp != nil && rc.req.Method != "HEAD" && resp.ContentLength != 0 if err != nil { pc.close() } else { if rc.addedGzip && hasBody && resp.Header.Get("Content-Encoding") == "gzip" { resp.Header.Del("Content-Encoding") resp.Header.Del("Content-Length") resp.ContentLength = -1 resp.Body = &gzipReader{body: resp.Body} } resp.Body = &bodyEOFSignal{body: resp.Body} } if err != nil || resp.Close || rc.req.Close || resp.StatusCode <= 199 { // Don't do keep-alive on error if either party requested a close // or we get an unexpected informational (1xx) response. // StatusCode 100 is already handled above. alive = false } var waitForBodyRead chan bool if hasBody { waitForBodyRead = make(chan bool, 2) resp.Body.(*bodyEOFSignal).earlyCloseFn = func() error { // Sending false here sets alive to // false and closes the connection // below. waitForBodyRead <- false return nil } resp.Body.(*bodyEOFSignal).fn = func(err error) { waitForBodyRead <- alive && err == nil && !pc.sawEOF && pc.wroteRequest() && pc.t.putIdleConn(pc) } } if alive && !hasBody { alive = !pc.sawEOF && pc.wroteRequest() && pc.t.putIdleConn(pc) } rc.ch <- responseAndError{resp, err} // Wait for the just-returned response body to be fully consumed // before we race and peek on the underlying bufio reader. if waitForBodyRead != nil { select { case alive = <-waitForBodyRead: case <-pc.closech: alive = false } } pc.t.setReqCanceler(rc.req, nil) if !alive { pc.close() } } } func (pc *persistConn) writeLoop() { for { select { case wr := <-pc.writech: if pc.isBroken() { wr.ch <- errors.New("http: can't write HTTP request on broken connection") continue } err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra) if err == nil { err = pc.bw.Flush() } if err != nil { pc.markBroken() wr.req.Request.closeBody() } pc.writeErrCh <- err // to the body reader, which might recycle us wr.ch <- err // to the roundTrip function case <-pc.closech: return } } } // wroteRequest is a check before recycling a connection that the previous write // (from writeLoop above) happened and was successful. func (pc *persistConn) wroteRequest() bool { select { case err := <-pc.writeErrCh: // Common case: the write happened well before the response, so // avoid creating a timer. return err == nil default: // Rare case: the request was written in writeLoop above but // before it could send to pc.writeErrCh, the reader read it // all, processed it, and called us here. In this case, give the // write goroutine a bit of time to finish its send. // // Less rare case: We also get here in the legitimate case of // Issue 7569, where the writer is still writing (or stalled), // but the server has already replied. In this case, we don't // want to wait too long, and we want to return false so this // connection isn't re-used. select { case err := <-pc.writeErrCh: return err == nil case <-time.After(50 * time.Millisecond): return false } } } type responseAndError struct { res *Response err error } type requestAndChan struct { req *Request ch chan responseAndError // did the Transport (as opposed to the client code) add an // Accept-Encoding gzip header? only if it we set it do // we transparently decode the gzip. addedGzip bool } // A writeRequest is sent by the readLoop's goroutine to the // writeLoop's goroutine to write a request while the read loop // concurrently waits on both the write response and the server's // reply. type writeRequest struct { req *transportRequest ch chan<- error } type httpError struct { err string timeout bool } func (e *httpError) Error() string { return e.err } func (e *httpError) Timeout() bool { return e.timeout } func (e *httpError) Temporary() bool { return true } var errTimeout error = &httpError{err: "net/http: timeout awaiting response headers", timeout: true} var errClosed error = &httpError{err: "net/http: transport closed before response was received"} func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) { pc.t.setReqCanceler(req.Request, pc.cancelRequest) pc.lk.Lock() pc.numExpectedResponses++ headerFn := pc.mutateHeaderFunc pc.lk.Unlock() if headerFn != nil { headerFn(req.extraHeaders()) } // Ask for a compressed version if the caller didn't set their // own value for Accept-Encoding. We only attempted to // uncompress the gzip stream if we were the layer that // requested it. requestedGzip := false if !pc.t.DisableCompression && req.Header.Get("Accept-Encoding") == "" && req.Method != "HEAD" { // Request gzip only, not deflate. Deflate is ambiguous and // not as universally supported anyway. // See: http://www.gzip.org/zlib/zlib_faq.html#faq38 // // Note that we don't request this for HEAD requests, // due to a bug in nginx: // http://trac.nginx.org/nginx/ticket/358 // http://golang.org/issue/5522 requestedGzip = true req.extraHeaders().Set("Accept-Encoding", "gzip") } // Write the request concurrently with waiting for a response, // in case the server decides to reply before reading our full // request body. writeErrCh := make(chan error, 1) pc.writech <- writeRequest{req, writeErrCh} resc := make(chan responseAndError, 1) pc.reqch <- requestAndChan{req.Request, resc, requestedGzip} var re responseAndError var pconnDeadCh = pc.closech var failTicker <-chan time.Time var respHeaderTimer <-chan time.Time WaitResponse: for { select { case err := <-writeErrCh: if err != nil { re = responseAndError{nil, err} pc.close() break WaitResponse } if d := pc.t.ResponseHeaderTimeout; d > 0 { respHeaderTimer = time.After(d) } case <-pconnDeadCh: // The persist connection is dead. This shouldn't // usually happen (only with Connection: close responses // with no response bodies), but if it does happen it // means either a) the remote server hung up on us // prematurely, or b) the readLoop sent us a response & // closed its closech at roughly the same time, and we // selected this case first, in which case a response // might still be coming soon. // // We can't avoid the select race in b) by using a unbuffered // resc channel instead, because then goroutines can // leak if we exit due to other errors. pconnDeadCh = nil // avoid spinning failTicker = time.After(100 * time.Millisecond) // arbitrary time to wait for resc case <-failTicker: re = responseAndError{err: errClosed} break WaitResponse case <-respHeaderTimer: pc.close() re = responseAndError{err: errTimeout} break WaitResponse case re = <-resc: break WaitResponse } } pc.lk.Lock() pc.numExpectedResponses-- pc.lk.Unlock() if re.err != nil { pc.t.setReqCanceler(req.Request, nil) } return re.res, re.err } // markBroken marks a connection as broken (so it's not reused). // It differs from close in that it doesn't close the underlying // connection for use when it's still being read. func (pc *persistConn) markBroken() { pc.lk.Lock() defer pc.lk.Unlock() pc.broken = true } func (pc *persistConn) close() { pc.lk.Lock() defer pc.lk.Unlock() pc.closeLocked() } func (pc *persistConn) closeLocked() { pc.broken = true if !pc.closed { pc.conn.Close() pc.closed = true close(pc.closech) } pc.mutateHeaderFunc = nil } var portMap = map[string]string{ "http": "80", "https": "443", } // canonicalAddr returns url.Host but always with a ":port" suffix func canonicalAddr(url *url.URL) string { addr := url.Host if !hasPort(addr) { return addr + ":" + portMap[url.Scheme] } return addr } // bodyEOFSignal wraps a ReadCloser but runs fn (if non-nil) at most // once, right before its final (error-producing) Read or Close call // returns. If earlyCloseFn is non-nil and Close is called before // io.EOF is seen, earlyCloseFn is called instead of fn, and its // return value is the return value from Close. type bodyEOFSignal struct { body io.ReadCloser mu sync.Mutex // guards following 4 fields closed bool // whether Close has been called rerr error // sticky Read error fn func(error) // error will be nil on Read io.EOF earlyCloseFn func() error // optional alt Close func used if io.EOF not seen } func (es *bodyEOFSignal) Read(p []byte) (n int, err error) { es.mu.Lock() closed, rerr := es.closed, es.rerr es.mu.Unlock() if closed { return 0, errors.New("http: read on closed response body") } if rerr != nil { return 0, rerr } n, err = es.body.Read(p) if err != nil { es.mu.Lock() defer es.mu.Unlock() if es.rerr == nil { es.rerr = err } es.condfn(err) } return } func (es *bodyEOFSignal) Close() error { es.mu.Lock() defer es.mu.Unlock() if es.closed { return nil } es.closed = true if es.earlyCloseFn != nil && es.rerr != io.EOF { return es.earlyCloseFn() } err := es.body.Close() es.condfn(err) return err } // caller must hold es.mu. func (es *bodyEOFSignal) condfn(err error) { if es.fn == nil { return } if err == io.EOF { err = nil } es.fn(err) es.fn = nil } // gzipReader wraps a response body so it can lazily // call gzip.NewReader on the first call to Read type gzipReader struct { body io.ReadCloser // underlying Response.Body zr io.Reader // lazily-initialized gzip reader } func (gz *gzipReader) Read(p []byte) (n int, err error) { if gz.zr == nil { gz.zr, err = gzip.NewReader(gz.body) if err != nil { return 0, err } } return gz.zr.Read(p) } func (gz *gzipReader) Close() error { return gz.body.Close() } type readerAndCloser struct { io.Reader io.Closer } type tlsHandshakeTimeoutError struct{} func (tlsHandshakeTimeoutError) Timeout() bool { return true } func (tlsHandshakeTimeoutError) Temporary() bool { return true } func (tlsHandshakeTimeoutError) Error() string { return "net/http: TLS handshake timeout" } type noteEOFReader struct { r io.Reader sawEOF *bool } func (nr noteEOFReader) Read(p []byte) (n int, err error) { n, err = nr.r.Read(p) if err == io.EOF { *nr.sawEOF = true } return } ubuntu-push-0.68+16.04.20160310.2/poller/0000755000015600001650000000000012670364532017750 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/poller/poller_test.go0000644000015600001650000001104412670364255022635 0ustar pbuserpbgroup00000000000000/* Copyright 2014-2015 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 poller import ( "testing" "time" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/client/session" helpers "launchpad.net/ubuntu-push/testing" ) // hook up gocheck func TestPoller(t *testing.T) { TestingT(t) } type PrSuite struct { log *helpers.TestLogger myd *myD } var _ = Suite(&PrSuite{}) type myD struct { // in/out for RequestWakeup reqWakeName string reqWakeTime time.Time reqWakeCookie string reqWakeErr error // WatchWakeups watchWakeCh chan bool watchWakeErr error // RequestWakelock reqLockName string reqLockCookie string reqLockErr error // ClearWakelock clearLockCookie string clearLockErr error // Poll pollErr error // WatchDones watchDonesCh <-chan bool watchDonesErr error // State stateState session.ClientSessionState } func (m *myD) RequestWakeup(name string, wakeupTime time.Time) (string, error) { m.reqWakeName = name m.reqWakeTime = wakeupTime time.AfterFunc(100*time.Millisecond, func() { m.watchWakeCh <- true }) return m.reqWakeCookie, m.reqWakeErr } func (m *myD) RequestWakelock(name string) (string, error) { m.reqLockName = name return m.reqLockCookie, m.reqLockErr } func (m *myD) ClearWakelock(cookie string) error { m.clearLockCookie = cookie return m.clearLockErr } func (m *myD) ClearWakeup(cookie string) error { m.watchWakeCh <- false return nil } func (m *myD) WatchWakeups() (<-chan bool, error) { return m.watchWakeCh, m.watchWakeErr } func (m *myD) Poll() error { return m.pollErr } func (m *myD) WatchDones() (<-chan bool, error) { return m.watchDonesCh, m.watchDonesErr } func (m *myD) State() session.ClientSessionState { return m.stateState } func (s *PrSuite) SetUpTest(c *C) { s.log = helpers.NewTestLogger(c, "debug") s.myd = &myD{} } func (s *PrSuite) TestStep(c *C) { p := &poller{ times: Times{}, log: s.log, powerd: s.myd, polld: s.myd, sessionState: s.myd, requestWakeupCh: make(chan struct{}), requestedWakeupErrCh: make(chan error), holdsWakeLockCh: make(chan bool), connCh: make(chan bool), } s.myd.reqLockCookie = "wakelock cookie" s.myd.stateState = session.Running wakeupCh := make(chan bool, 1) s.myd.watchWakeCh = wakeupCh // we won't get the "done" signal in time ;) doneCh := make(chan bool) // and a channel to get the return value from a goroutine ch := make(chan string) // now, run filteredWakeUpCh := make(chan bool) go p.control(wakeupCh, filteredWakeUpCh) go func() { ch <- p.step(filteredWakeUpCh, doneCh, "old cookie") }() select { case s := <-ch: c.Check(s, Equals, "wakelock cookie") case <-time.After(time.Second): c.Fatal("timeout waiting for step") } // check we cleared the old cookie c.Check(s.myd.clearLockCookie, Equals, "old cookie") } func (s *PrSuite) TestControl(c *C) { p := &poller{ times: Times{}, log: s.log, powerd: s.myd, polld: s.myd, sessionState: s.myd, requestWakeupCh: make(chan struct{}), requestedWakeupErrCh: make(chan error), holdsWakeLockCh: make(chan bool), connCh: make(chan bool), } wakeUpCh := make(chan bool) filteredWakeUpCh := make(chan bool) s.myd.watchWakeCh = make(chan bool, 1) go p.control(wakeUpCh, filteredWakeUpCh) // works p.HasConnectivity(true) err := p.requestWakeup() c.Assert(err, IsNil) c.Check(<-s.myd.watchWakeCh, Equals, true) // there's a wakeup already err = p.requestWakeup() c.Assert(err, IsNil) c.Check(s.myd.watchWakeCh, HasLen, 0) // wakeup happens wakeUpCh <- true <-filteredWakeUpCh p.HasConnectivity(false) err = p.requestWakeup() c.Assert(err, IsNil) c.Check(s.myd.watchWakeCh, HasLen, 0) // connected p.HasConnectivity(true) c.Check(<-s.myd.watchWakeCh, Equals, true) // disconnected p.HasConnectivity(false) // pending wakeup was cleared c.Check(<-s.myd.watchWakeCh, Equals, false) } ubuntu-push-0.68+16.04.20160310.2/poller/poller.go0000644000015600001650000002114312670364255021577 0ustar pbuserpbgroup00000000000000/* Copyright 2014-2015 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 poller implements Poller, a thing that uses (hw) alarms to // wake the device up from deep sleep periodically, check for // notifications, and poke polld. package poller import ( "errors" "sync" "time" "launchpad.net/ubuntu-push/bus" "launchpad.net/ubuntu-push/bus/polld" "launchpad.net/ubuntu-push/bus/powerd" "launchpad.net/ubuntu-push/client/session" "launchpad.net/ubuntu-push/logger" "launchpad.net/ubuntu-push/util" ) var ( ErrUnconfigured = errors.New("not configured") ErrAlreadyStarted = errors.New("already started") ErrNotStarted = errors.New("not started") ) type stater interface { State() session.ClientSessionState } type Times struct { AlarmInterval time.Duration SessionStateSettle time.Duration NetworkWait time.Duration PolldWait time.Duration DoneWait time.Duration BusyWait time.Duration } type Poller interface { IsConnected() bool Start() error Run() error HasConnectivity(bool) } type PollerSetup struct { Times Times Log logger.Logger SessionStateGetter stater } type poller struct { times Times log logger.Logger powerd powerd.Powerd polld polld.Polld cookie string sessionState stater connCh chan bool requestWakeupCh chan struct{} requestedWakeupErrCh chan error holdsWakeLockCh chan bool } func New(setup *PollerSetup) Poller { return &poller{ times: setup.Times, log: setup.Log, powerd: nil, polld: nil, sessionState: setup.SessionStateGetter, connCh: make(chan bool), requestWakeupCh: make(chan struct{}), requestedWakeupErrCh: make(chan error), holdsWakeLockCh: make(chan bool), } } func (p *poller) IsConnected() bool { return p.sessionState.State() == session.Running } func (p *poller) HasConnectivity(hasConn bool) { p.connCh <- hasConn } func (p *poller) Start() error { if p.log == nil { return ErrUnconfigured } if p.powerd != nil || p.polld != nil { return ErrAlreadyStarted } powerdEndp := bus.SystemBus.Endpoint(powerd.BusAddress, p.log) polldEndp := bus.SessionBus.Endpoint(polld.BusAddress, p.log) var wg sync.WaitGroup wg.Add(2) go func() { n := util.NewAutoRedialer(powerdEndp).Redial() p.log.Debugf("powerd dialed on try %d", n) wg.Done() }() go func() { n := util.NewAutoRedialer(polldEndp).Redial() p.log.Debugf("polld dialed in on try %d", n) wg.Done() }() wg.Wait() p.powerd = powerd.New(powerdEndp, p.log) p.polld = polld.New(polldEndp, p.log) // busy sleep loop to workaround go's timer/sleep // not accounting for time when the system is suspended // see https://bugs.launchpad.net/ubuntu/+source/ubuntu-push/+bug/1435109 if p.times.BusyWait > 0 { p.log.Debugf("starting busy loop with %s interval", p.times.BusyWait) go func() { for { time.Sleep(p.times.BusyWait) } }() } else { p.log.Debugf("skipping busy loop") } return nil } func (p *poller) Run() error { if p.log == nil { return ErrUnconfigured } if p.powerd == nil || p.polld == nil { return ErrNotStarted } wakeupCh, err := p.powerd.WatchWakeups() if err != nil { return err } doneCh, err := p.polld.WatchDones() if err != nil { return err } filteredWakeUpCh := make(chan bool) go p.control(wakeupCh, filteredWakeUpCh) go p.run(filteredWakeUpCh, doneCh) return nil } func (p *poller) doRequestWakeup(delta time.Duration) (time.Time, string, error) { t := time.Now().Add(delta).Truncate(time.Second) cookie, err := p.powerd.RequestWakeup("ubuntu push client", t) if err == nil { p.log.Debugf("requested wakeup at %s", t) } else { p.log.Errorf("RequestWakeup got %v", err) t = time.Time{} cookie = "" } return t, cookie, err } func (p *poller) control(wakeupCh <-chan bool, filteredWakeUpCh chan<- bool) { // Assume a connection, and poll immediately. connected := true dontPoll := !connected var t time.Time cookie := "" holdsWakeLock := false for { select { case holdsWakeLock = <-p.holdsWakeLockCh: case <-p.requestWakeupCh: if !t.IsZero() || dontPoll { // earlier wakeup or we shouldn't be polling // => don't request wakeup if dontPoll { p.log.Debugf("skip requesting wakeup") } p.requestedWakeupErrCh <- nil break } var err error t, cookie, err = p.doRequestWakeup(p.times.AlarmInterval) p.requestedWakeupErrCh <- err case b := <-wakeupCh: // seems we get here also on clear wakeup, oh well if !b { panic("WatchWakeups channel produced a false value (??)") } // the channel will produce a true for every // wakeup, not only the one we asked for now := time.Now() if t.IsZero() { p.log.Debugf("got woken up; time is %s", now) } else { p.log.Debugf("got woken up; time is %s (ð›¥: %s)", now, now.Sub(t)) if !now.Before(t) { t = time.Time{} filteredWakeUpCh <- true } } case state := <-p.connCh: connected = state p.log.Debugf("control: connected:%v", state) } newDontPoll := !connected p.log.Debugf("control: prevDontPoll:%v dontPoll:%v wakeupReq:%v holdsWakeLock:%v", dontPoll, newDontPoll, !t.IsZero(), holdsWakeLock) if newDontPoll != dontPoll { if dontPoll = newDontPoll; dontPoll { if !t.IsZero() { err := p.powerd.ClearWakeup(cookie) if err == nil { // cleared t = time.Time{} p.log.Debugf("cleared wakeup") } else { p.log.Errorf("ClearWakeup got %v", err) } } } else { if t.IsZero() && !holdsWakeLock { // reschedule soon var err error t, cookie, err = p.doRequestWakeup(p.times.NetworkWait / 20) if err != nil { // Make sure we break a potential deadlock by trying again. filteredWakeUpCh <- true } } } } } } func (p *poller) requestWakeup() error { p.requestWakeupCh <- struct{}{} return <-p.requestedWakeupErrCh } func (p *poller) holdsWakeLock(has bool) { p.holdsWakeLockCh <- has } func (p *poller) run(wakeupCh <-chan bool, doneCh <-chan bool) { var lockCookie string for { lockCookie = p.step(wakeupCh, doneCh, lockCookie) } } func (p *poller) step(wakeupCh <-chan bool, doneCh <-chan bool, lockCookie string) string { err := p.requestWakeup() if err != nil { // Don't do this too quickly. Pretend we are just skipping one wakeup time.Sleep(p.times.AlarmInterval) return lockCookie } p.holdsWakeLock(false) if lockCookie != "" { if err := p.powerd.ClearWakelock(lockCookie); err != nil { p.log.Errorf("ClearWakelock(%#v) got %v", lockCookie, err) } lockCookie = "" } <-wakeupCh lockCookie, err = p.powerd.RequestWakelock("ubuntu push client") if err != nil { p.log.Errorf("RequestWakelock got %v", err) return lockCookie } p.holdsWakeLock(true) p.log.Debugf("got wakelock cookie of %s, checking conn state", lockCookie) time.Sleep(p.times.SessionStateSettle) for i := 0; i < 20; i++ { if p.IsConnected() { p.log.Debugf("iter %02d: connected", i) break } p.log.Debugf("iter %02d: not connected, sleeping for %s", i, p.times.NetworkWait/20) time.Sleep(p.times.NetworkWait / 20) p.log.Debugf("iter %02d: slept", i) } if !p.IsConnected() { p.log.Errorf("not connected after %s; giving up", p.times.NetworkWait) } else { p.log.Debugf("poking polld.") // drain the doneCH drain: for { select { case <-doneCh: default: break drain } } if err := p.polld.Poll(); err != nil { p.log.Errorf("Poll got %v", err) } else { p.log.Debugf("waiting for polld to signal Done.") select { case b := <-doneCh: if !b { panic("WatchDones channel produced a false value (??)") } p.log.Debugf("polld Done.") case <-time.After(p.times.PolldWait): p.log.Errorf("polld still not done after %s; giving up", p.times.PolldWait) } } // XXX check whether something was actually done before waiting p.log.Debugf("sleeping for DoneWait %s", p.times.DoneWait) time.Sleep(p.times.DoneWait) p.log.Debugf("slept") } return lockCookie } ubuntu-push-0.68+16.04.20160310.2/click/0000755000015600001650000000000012670364532017540 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/click/cclick/0000755000015600001650000000000012670364532020770 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/click/cclick/cclick.go0000644000015600001650000000341012670364255022547 0ustar pbuserpbgroup00000000000000/* 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 cclick has the internal cgo wrapping libclick for package click. package cclick /* #cgo pkg-config: click-0.4 #cgo pkg-config: glib-2.0 gobject-2.0 #include */ import "C" import ( "fmt" "runtime" ) type CClickUser struct { cref *C.ClickUser } func gchar(s string) *C.gchar { return (*C.gchar)(C.CString(s)) } func gfree(s *C.gchar) { C.g_free((C.gpointer)(s)) } func (ccu *CClickUser) CInit(holder interface{}) error { var gerr *C.GError cref := C.click_user_new_for_user(nil, nil, &gerr) defer C.g_clear_error(&gerr) if gerr != nil { return fmt.Errorf("faild to make ClickUser: %s", C.GoString((*C.char)(gerr.message))) } ccu.cref = cref runtime.SetFinalizer(holder, func(interface{}) { ccu.cref = nil // blocks gc really for now, 1.3 otherwise panics C.g_object_unref((C.gpointer)(cref)) }) return nil } func (ccu *CClickUser) CGetVersion(pkgName string) string { pkgname := gchar(pkgName) defer gfree(pkgname) var gerr *C.GError defer C.g_clear_error(&gerr) ver := C.click_user_get_version(ccu.cref, pkgname, &gerr) if gerr != nil { return "" } defer gfree(ver) return C.GoString((*C.char)(ver)) } ubuntu-push-0.68+16.04.20160310.2/click/cblacklist/0000755000015600001650000000000012670364532021653 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/click/cblacklist/cblacklist.go0000644000015600001650000000443612670364255024326 0ustar pbuserpbgroup00000000000000/* 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 cblacklist accesses the g_settings notification blacklist package cblacklist /* #cgo pkg-config: gio-unix-2.0 #cgo pkg-config: glib-2.0 #include #include #define BLACKLIST_CONFIG_SCHEMA_ID "com.ubuntu.notifications.hub" #define BLACKLIST_KEY "blacklist" int is_blacklisted(const char *pkgname, const char *appname) { static GSettings *pushSettings = NULL; GVariantIter *iter; gchar *pkg; gchar *app; int blacklisted = 0; if (!pushSettings) { GSettingsSchemaSource * source = g_settings_schema_source_get_default (); if (!g_settings_schema_source_lookup (g_settings_schema_source_get_default (), BLACKLIST_CONFIG_SCHEMA_ID, TRUE)) { return -1; } pushSettings = g_settings_new(BLACKLIST_CONFIG_SCHEMA_ID); } GVariant *blacklist = g_settings_get_value(pushSettings, BLACKLIST_KEY); g_variant_get (blacklist, "a(ss)", &iter); while (g_variant_iter_loop (iter, "(ss)", &pkg, &app)) { if (0==g_strcmp0(pkg, pkgname) && 0==g_strcmp0(app, appname)) { blacklisted = 1; break; } // No need to free pkg and app, according to GVariant array example } g_variant_iter_free (iter); g_variant_unref (blacklist); return blacklisted; } */ import "C" import ( "unsafe" "launchpad.net/ubuntu-push/click" ) // IsBlacklisted returns true if the application is in the gsettings blacklist func IsBlacklisted(app *click.AppId) bool { pkgname := C.CString(app.Package) appname := C.CString(app.Application) defer C.free(unsafe.Pointer(pkgname)) defer C.free(unsafe.Pointer(appname)) return C.is_blacklisted(pkgname, appname) == 1 } ubuntu-push-0.68+16.04.20160310.2/click/click.go0000644000015600001650000001204112670364255021154 0ustar pbuserpbgroup00000000000000/* 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 click exposes some utilities related to click packages and // wraps libclick to check if packages are installed. package click import ( "encoding/json" "errors" "fmt" "path/filepath" "regexp" "strings" "sync" "launchpad.net/go-xdg/v0" "launchpad.net/ubuntu-push/click/cappinfo" "launchpad.net/ubuntu-push/click/cclick" ) // AppId holds a parsed application id. type AppId struct { Package string Application string Version string Click bool original string } // from https://wiki.ubuntu.com/AppStore/Interfaces/ApplicationId // except the version is made optional var rxClick = regexp.MustCompile(`^([a-z0-9][a-z0-9+.-]+)_([a-zA-Z0-9+.-]+)(?:_([0-9][a-zA-Z0-9.+:~-]*))?$`) // no / and not starting with . var rxLegacy = regexp.MustCompile(`^[^./][^/]*$`) var ( ErrInvalidAppId = errors.New("invalid application id") ErrMissingApp = errors.New("application not installed") ) func ParseAppId(id string) (*AppId, error) { app := new(AppId) err := app.setFromString(id) if err == nil { return app, nil } else { return nil, err } } func (app *AppId) setFromString(id string) error { if strings.HasPrefix(id, "_") { // legacy appname := id[1:] if !rxLegacy.MatchString(appname) { return ErrInvalidAppId } app.Package = "" app.Application = appname app.Version = "" app.Click = false app.original = id return nil } else { m := rxClick.FindStringSubmatch(id) if len(m) == 0 { return ErrInvalidAppId } app.Package = m[1] app.Application = m[2] app.Version = m[3] app.Click = true app.original = id return nil } } func (app *AppId) InPackage(pkgname string) bool { return app.Package == pkgname } func (app *AppId) DispatchPackage() string { if app.Click { return app.Package } return app.Application } func (app *AppId) Original() string { return app.original } func (app *AppId) String() string { return app.Original() } func (app *AppId) Base() string { if app.Click { return app.Package + "_" + app.Application } else { return app.Application } } func (app *AppId) Versioned() string { if app.Click { if app.Version == "" { panic(fmt.Errorf("Versioned() on AppId without version/not verified: %#v", app)) } return app.Package + "_" + app.Application + "_" + app.Version } else { return app.Application } } func (app *AppId) DesktopId() string { return app.Versioned() + ".desktop" } func (app *AppId) Icon() string { return cappinfo.AppIconFromDesktopId(app.DesktopId()) } func _symbolic(icon string) string { if strings.ContainsRune(icon, '/') { return icon } return icon + "-symbolic" } var symbolic = _symbolic func (app *AppId) SymbolicIcon() string { symbolicIcon := cappinfo.AppSymbolicIconFromDesktopId(app.DesktopId()) if symbolicIcon != "" { return symbolicIcon } return symbolic(app.Icon()) } func (app *AppId) MarshalJSON() ([]byte, error) { return json.Marshal(app.Original()) } func (app *AppId) UnmarshalJSON(s []byte) error { var v string err := json.Unmarshal(s, &v) if err != nil { return err } return app.setFromString(v) } // ClickUser exposes the click package registry for the user. type ClickUser struct { ccu cclick.CClickUser lock sync.Mutex } type InstalledChecker interface { Installed(app *AppId, setVersion bool) bool } // ParseAndVerifyAppId parses the given app id and checks if the // corresponding app is installed, returning the parsed id or // ErrInvalidAppId, or the parsed id and ErrMissingApp respectively. func ParseAndVerifyAppId(id string, installedChecker InstalledChecker) (*AppId, error) { app, err := ParseAppId(id) if err != nil { return nil, err } if installedChecker != nil && !installedChecker.Installed(app, true) { return app, ErrMissingApp } return app, nil } // User makes a new ClickUser object for the current user. func User() (*ClickUser, error) { cu := new(ClickUser) err := cu.ccu.CInit(cu) if err != nil { return nil, err } return cu, nil } // Installed checks if the appId is installed for user, optionally setting // the version if it was absent. func (cu *ClickUser) Installed(app *AppId, setVersion bool) bool { cu.lock.Lock() defer cu.lock.Unlock() if app.Click { ver := cu.ccu.CGetVersion(app.Package) if ver == "" { return false } if app.Version != "" { return app.Version == ver } else if setVersion { app.Version = ver } return true } else { _, err := xdg.Data.Find(filepath.Join("applications", app.DesktopId())) return err == nil } } ubuntu-push-0.68+16.04.20160310.2/click/testing/0000755000015600001650000000000012670364532021215 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/click/testing/helpers.go0000644000015600001650000000164412670364255023215 0ustar pbuserpbgroup00000000000000/* 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 testing contains helpers for testing related to click. package testing import ( "launchpad.net/ubuntu-push/click" ) // MustParseAppId parses an appId or panics. func MustParseAppId(appId string) *click.AppId { app, err := click.ParseAppId(appId) if err != nil { panic(err) } return app } ubuntu-push-0.68+16.04.20160310.2/click/cappinfo/0000755000015600001650000000000012670364532021337 5ustar pbuserpbgroup00000000000000ubuntu-push-0.68+16.04.20160310.2/click/cappinfo/cappinfo.go0000644000015600001650000000471712670364255023500 0ustar pbuserpbgroup00000000000000/* 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 cappinfo wraps C functions to access app information package cappinfo /* #cgo pkg-config: gio-unix-2.0 #include #include gchar* app_icon_filename_from_desktop_id (gchar* desktop_id) { gchar* filename = NULL; GAppInfo* app_info = (GAppInfo*)g_desktop_app_info_new (desktop_id); if (app_info != NULL) { GIcon* icon = g_app_info_get_icon (app_info); if (icon != NULL) { filename = g_icon_to_string (icon); // g_app_info_get_icon has "transfer none" } g_object_unref (app_info); } g_free (desktop_id); return filename; } gchar* app_symbolic_icon_from_desktop_id (gchar* desktop_id) { gchar* x_symbolic_icon; GIcon* symbolic_icon; GDesktopAppInfo* app_info = g_desktop_app_info_new (desktop_id); if (app_info != NULL) { if((x_symbolic_icon = g_desktop_app_info_get_string(app_info, "X-Ubuntu-SymbolicIcon"))) { GFile *file; file = g_file_new_for_path(x_symbolic_icon); symbolic_icon = g_file_icon_new (file); g_object_unref (file); g_free(x_symbolic_icon); g_object_unref (app_info); return g_icon_to_string(symbolic_icon); } g_object_unref (app_info); } g_free (desktop_id); return NULL; } */ import "C" func AppIconFromDesktopId(desktopId string) string { name := C.app_icon_filename_from_desktop_id((*C.gchar)(C.CString(desktopId))) defer C.g_free((C.gpointer)(name)) return C.GoString((*C.char)(name)) } func appSymbolicIconFromDesktopId(desktopId string) string { name := C.app_symbolic_icon_from_desktop_id((*C.gchar)(C.CString(desktopId))) if name == nil { return "" } defer C.g_free((C.gpointer)(name)) return C.GoString((*C.char)(name)) } var AppSymbolicIconFromDesktopId = appSymbolicIconFromDesktopId ubuntu-push-0.68+16.04.20160310.2/click/click_test.go0000644000015600001650000001604212670364255022220 0ustar pbuserpbgroup00000000000000/* Copyright 2013-2015 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 click import ( "encoding/json" "fmt" "os/exec" "strings" "testing" . "launchpad.net/gocheck" "launchpad.net/ubuntu-push/click/cappinfo" ) func TestClick(t *testing.T) { TestingT(t) } type clickSuite struct{} var _ = Suite(&clickSuite{}) func GetPyVer() string { out, err := exec.Command("python3", "-V").Output() if err != nil { panic(err) } pyver := strings.Replace(string(out[:]), "Python ", "", -1) vers := strings.Split(pyver, ".") return fmt.Sprintf("%s.%s", vers[0], vers[1]) } func (cs *clickSuite) TestParseAppId(c *C) { app, err := ParseAppId("com.ubuntu.clock_clock") c.Assert(err, IsNil) c.Check(app.Package, Equals, "com.ubuntu.clock") c.Check(app.InPackage("com.ubuntu.clock"), Equals, true) c.Check(app.Application, Equals, "clock") c.Check(app.Version, Equals, "") c.Check(app.Click, Equals, true) c.Check(app.Original(), Equals, "com.ubuntu.clock_clock") c.Check(fmt.Sprintf("%s", app), Equals, "com.ubuntu.clock_clock") c.Check(app.DispatchPackage(), Equals, "com.ubuntu.clock") app, err = ParseAppId("com.ubuntu.clock_clock_10") c.Assert(err, IsNil) c.Check(app.Package, Equals, "com.ubuntu.clock") c.Check(app.InPackage("com.ubuntu.clock"), Equals, true) c.Check(app.Application, Equals, "clock") c.Check(app.Version, Equals, "10") c.Check(app.Click, Equals, true) c.Check(app.Original(), Equals, "com.ubuntu.clock_clock_10") c.Check(fmt.Sprintf("%s", app), Equals, "com.ubuntu.clock_clock_10") c.Check(app.Versioned(), Equals, "com.ubuntu.clock_clock_10") c.Check(app.Base(), Equals, "com.ubuntu.clock_clock") c.Check(app.DesktopId(), Equals, "com.ubuntu.clock_clock_10.desktop") c.Check(app.DispatchPackage(), Equals, "com.ubuntu.clock") for _, s := range []string{"com.ubuntu.clock_clock_10_4", "com.ubuntu.clock", ""} { app, err = ParseAppId(s) c.Check(app, IsNil) c.Check(err, Equals, ErrInvalidAppId) } } func (cs *clickSuite) TestVersionedPanic(c *C) { app, err := ParseAppId("com.ubuntu.clock_clock") c.Assert(err, IsNil) c.Check(func() { app.Versioned() }, PanicMatches, `Versioned\(\) on AppId without version/not verified:.*`) } func (cs *clickSuite) TestParseAppIdLegacy(c *C) { pyver := fmt.Sprintf("python%s", GetPyVer()) app, err := ParseAppId(fmt.Sprintf("_%s", pyver)) c.Assert(err, IsNil) c.Check(app.Package, Equals, "") c.Check(app.InPackage(""), Equals, true) c.Check(app.Application, Equals, pyver) c.Check(app.Version, Equals, "") c.Check(app.Click, Equals, false) c.Check(app.Original(), Equals, fmt.Sprintf("_%s", pyver)) c.Check(app.Versioned(), Equals, pyver) c.Check(app.Base(), Equals, pyver) c.Check(app.DesktopId(), Equals, fmt.Sprintf("%s.desktop", pyver)) c.Check(app.DispatchPackage(), Equals, pyver) for _, s := range []string{"_.foo", "_foo/", "_/foo"} { app, err = ParseAppId(s) c.Check(app, IsNil) c.Check(err, Equals, ErrInvalidAppId) } } func (cs *clickSuite) TestJSON(c *C) { pyver := fmt.Sprintf("python%s", GetPyVer()) for _, appId := range []string{"com.ubuntu.clock_clock", "com.ubuntu.clock_clock_10", fmt.Sprintf("_%s", pyver)} { app, err := ParseAppId(appId) c.Assert(err, IsNil, Commentf(appId)) b, err := json.Marshal(app) c.Assert(err, IsNil, Commentf(appId)) var vapp *AppId err = json.Unmarshal(b, &vapp) c.Assert(err, IsNil, Commentf(appId)) c.Check(vapp, DeepEquals, app) } } func (cs *clickSuite) TestIcon(c *C) { pyver := fmt.Sprintf("python%s", GetPyVer()) app, err := ParseAppId(fmt.Sprintf("_%s", pyver)) c.Assert(err, IsNil) c.Check(app.Icon(), Equals, fmt.Sprintf("/usr/share/pixmaps/%s.xpm", pyver)) } func (s *clickSuite) TestUser(c *C) { u, err := User() c.Assert(err, IsNil) c.Assert(u, NotNil) } func (s *clickSuite) TestInstalledNegative(c *C) { u, err := User() c.Assert(err, IsNil) app, err := ParseAppId("com.foo.bar_baz") c.Assert(err, IsNil) c.Check(u.Installed(app, false), Equals, false) } func (s *clickSuite) TestInstalledVersionNegative(c *C) { u, err := User() c.Assert(err, IsNil) app, err := ParseAppId("com.ubuntu.clock_clock_1000.0") c.Assert(err, IsNil) c.Check(u.Installed(app, false), Equals, false) } func (s *clickSuite) TestInstalledClock(c *C) { u, err := User() c.Assert(err, IsNil) ver := u.ccu.CGetVersion("com.ubuntu.clock") if ver == "" { c.Skip("no com.ubuntu.clock pkg installed") } app, err := ParseAppId("com.ubuntu.clock_clock") c.Assert(err, IsNil) c.Check(u.Installed(app, false), Equals, true) app, err = ParseAppId("com.ubuntu.clock_clock_" + ver) c.Assert(err, IsNil) c.Check(u.Installed(app, false), Equals, true) app, err = ParseAppId("com.ubuntu.clock_clock_10" + ver) c.Assert(err, IsNil) c.Check(u.Installed(app, false), Equals, false) // setVersion app, err = ParseAppId("com.ubuntu.clock_clock") c.Assert(err, IsNil) c.Check(u.Installed(app, true), Equals, true) c.Check(app.Version, Equals, ver) } func (s *clickSuite) TestInstalledLegacy(c *C) { u, err := User() c.Assert(err, IsNil) app, err := ParseAppId(fmt.Sprintf("_python%s", GetPyVer())) c.Assert(err, IsNil) c.Check(u.Installed(app, false), Equals, true) } func (s *clickSuite) TestParseAndVerifyAppId(c *C) { u, err := User() c.Assert(err, IsNil) app, err := ParseAndVerifyAppId("_.foo", nil) c.Assert(err, Equals, ErrInvalidAppId) c.Check(app, IsNil) app, err = ParseAndVerifyAppId("com.foo.bar_baz", nil) c.Assert(err, IsNil) c.Check(app.Click, Equals, true) c.Check(app.Application, Equals, "baz") app, err = ParseAndVerifyAppId("_non-existent-app", u) c.Assert(err, Equals, ErrMissingApp) c.Check(app, NotNil) c.Check(app.Original(), Equals, "_non-existent-app") } func (s *clickSuite) TestSymbolicAppendsSymbolicIfIconIsName(c *C) { symb := symbolic("foo") c.Check(symb, Equals, "foo-symbolic") } func (s *clickSuite) TestSymbolicLeavesAloneIfIconIsPath(c *C) { symb := symbolic("foo/bar") c.Check(symb, Equals, "foo/bar") } func (s *clickSuite) TestSymbolicIconCallsSymbolic(c *C) { symbolic = func(string) string { return "xyzzy" } defer func() { symbolic = _symbolic }() app, err := ParseAppId(fmt.Sprintf("_python%s", GetPyVer())) c.Assert(err, IsNil) c.Check(app.SymbolicIcon(), Equals, "xyzzy") } func (s *clickSuite) TestSymbolicFromDesktopFile(c *C) { orig := cappinfo.AppSymbolicIconFromDesktopId cappinfo.AppSymbolicIconFromDesktopId = func(desktopId string) string { return "/foo/symbolic" } defer func() { cappinfo.AppSymbolicIconFromDesktopId = orig }() app, _ := ParseAppId("com.ubuntu.clock_clock_1.2") c.Check(app.SymbolicIcon(), Equals, "/foo/symbolic") }