From fb2b6cb8bbc9a4f99af826c809abc388b66b4e53 Mon Sep 17 00:00:00 2001 From: "H. Shay" Date: Thu, 15 Apr 2021 11:08:56 -0700 Subject: [PATCH] add coverage files to gitignore --- .coverage | Bin 53248 -> 0 bytes .gitignore | 2 + htmlcov/coverage_html.js | 616 ------------------ htmlcov/favicon_32.png | Bin 1732 -> 0 bytes htmlcov/index.html | 190 ------ htmlcov/jquery.ba-throttle-debounce.min.js | 9 - htmlcov/jquery.hotkeys.js | 99 --- htmlcov/jquery.isonscreen.js | 53 -- htmlcov/jquery.min.js | 5 - htmlcov/jquery.tablesorter.min.js | 2 - htmlcov/keybd_closed.png | Bin 112 -> 0 bytes htmlcov/keybd_open.png | Bin 112 -> 0 bytes htmlcov/status.json | 1 - htmlcov/style.css | 291 --------- htmlcov/sygnal___init___py.html | 87 --- htmlcov/sygnal_apnspushkin_py.html | 542 --------------- htmlcov/sygnal_apnstruncate_py.html | 197 ------ htmlcov/sygnal_exceptions_py.html | 112 ---- htmlcov/sygnal_gcmpushkin_py.html | 602 ----------------- htmlcov/sygnal_helper___init___py.html | 65 -- htmlcov/sygnal_helper_context_factory_py.html | 221 ------- htmlcov/sygnal_helper_proxy___init___py.html | 129 ---- ...r_proxy_connectproxyclient_twisted_py.html | 310 --------- .../sygnal_helper_proxy_proxy_asyncio_py.html | 447 ------------- ...al_helper_proxy_proxyagent_twisted_py.html | 230 ------- htmlcov/sygnal_http_py.html | 423 ------------ htmlcov/sygnal_notifications_py.html | 268 -------- htmlcov/sygnal_sygnal_py.html | 436 ------------- htmlcov/sygnal_utils_py.html | 139 ---- htmlcov/sygnal_webpushpushkin_py.html | 465 ------------- tox.ini | 1 + 31 files changed, 3 insertions(+), 5939 deletions(-) delete mode 100644 .coverage delete mode 100644 htmlcov/coverage_html.js delete mode 100644 htmlcov/favicon_32.png delete mode 100644 htmlcov/index.html delete mode 100644 htmlcov/jquery.ba-throttle-debounce.min.js delete mode 100644 htmlcov/jquery.hotkeys.js delete mode 100644 htmlcov/jquery.isonscreen.js delete mode 100644 htmlcov/jquery.min.js delete mode 100644 htmlcov/jquery.tablesorter.min.js delete mode 100644 htmlcov/keybd_closed.png delete mode 100644 htmlcov/keybd_open.png delete mode 100644 htmlcov/status.json delete mode 100644 htmlcov/style.css delete mode 100644 htmlcov/sygnal___init___py.html delete mode 100644 htmlcov/sygnal_apnspushkin_py.html delete mode 100644 htmlcov/sygnal_apnstruncate_py.html delete mode 100644 htmlcov/sygnal_exceptions_py.html delete mode 100644 htmlcov/sygnal_gcmpushkin_py.html delete mode 100644 htmlcov/sygnal_helper___init___py.html delete mode 100644 htmlcov/sygnal_helper_context_factory_py.html delete mode 100644 htmlcov/sygnal_helper_proxy___init___py.html delete mode 100644 htmlcov/sygnal_helper_proxy_connectproxyclient_twisted_py.html delete mode 100644 htmlcov/sygnal_helper_proxy_proxy_asyncio_py.html delete mode 100644 htmlcov/sygnal_helper_proxy_proxyagent_twisted_py.html delete mode 100644 htmlcov/sygnal_http_py.html delete mode 100644 htmlcov/sygnal_notifications_py.html delete mode 100644 htmlcov/sygnal_sygnal_py.html delete mode 100644 htmlcov/sygnal_utils_py.html delete mode 100644 htmlcov/sygnal_webpushpushkin_py.html diff --git a/.coverage b/.coverage deleted file mode 100644 index 7f4c2b399a770792c67246d60747626ab2cf1a37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI4YitzP6@c&TjQ2fz-4L@7N<2SGjfK5ydu?!N8`c4mLZT7~B~@SR+1*+1;N6+^ z%q+Is5WJ)kO4U?RDn%tPxj)*Zs%WCVAW;I1`lC{dnt;@2Dxhjb`O&CNiAowE?z!{u zdI970j|v5yEAQT!x#ygF?ss2%=Z@!r?f0pcA{BI_EL&2Fv=WXZX-Oi4MBpRB#|r^C z5WEwJxjD!49Y#pvo)<*Ai$wiT68dHFbF@49T`?K?Q*=-0Tao7jyF(>Nz<>mh01`j~ zNWdmg+a3+XmoMk`@3rJ?NwEw$rx?z+xN*nkp`DwhokMrsw^?$oNvp#Wgzj!>NHX+s zsiGKCK`kkgs^!(3Y^mCaWEB-px@szUHsirHnDdxm!q#U;88r`DmNEh+Du!B?jY(-# zne4I!+}tc>!g4Nv5k(!*n1r<2RqvDxrJxv!mQzf}i`8nrGu-*ezHlJEWC?dbvCAYI zIrxh-lm;%;i?XEvhH|=*XWnNGS<4j_vrCd^OPJHa&k4&Zt)Z8bmM)c4%`T>9s+Ov2 zk}{#>s+N+!W&yhxG%omBVj;5Q}96EL7YiPRSu5tmK({vAOU6ExC%C^s1 zwVVayyQ}32s~pg9GPd;jH0RT`bhofg*UdHRvR>YX>hzGa8g;a)nmSfhW-Gr-s%v)H z#nALFi4_7`z^aHpe#a7yK&wpHlg!4*o(|g;Z%$#O`%dUFm`iruu~@oHl{f>m~|+4 zQtnTja3@>fbMzvCKMY6!2_OL^fCP{L5sjqkaz#Z#*dPN_SZNq7Ujug#n5*vIm=9% zlOvj3N+v9QB3YSCmeednR#DediNPd1^`~@KOJOLHKu;2SlDGT6?&`#3Mm8t4oT@|4_lsa; z)gq0++X^(;qO~pNN~)q+8FvR3EPgr)7TXqS(KZEJxeb=?jDRI>k(M$URZ}f{^LIlS z^!cXsy_Yc*$2?3n}fhS`NXQAnqTCm?f1TPl>wJx3~nT6GgmQ?}iejiWt zg{4{Bw+wiN3A~{Iw0bz6j4Z4*r%u@Ze`{1B^o{7V^dKd%a;#rGFaB7JM^B0>9i{!T z@5D}k00tz01dsp{Kmter2_OL^fCP|0GX&Q0W)ln6tJeRm{d~Czy~g!F-6Yf^O=w+r z{U6&d)K)Fl$aUBM;ufLSwpfeTT>nQm2sLi8mgcPZB59$KDw`CJn^v+Msrf|r|+_tyXZ=3Hu6|F^8=$C}9E%Jsj{&1afWbJzd;0IxS8 zeZ~6U*Tav57PbDz_y09BfT0W$Kmter2_OL^fCP{L5bDNB{{S0VIF~kN^@u0!RP}AOR%s;U;hwZQ(*SffDk;W%2jN z&oBGg<-@PPE)(+Ddjh$2iX7T@&%leRjfb;mkDfpNH2KMRcOgH%|Kxzx&7GWoLFV$6k6WOnmzXhsc?6olC#8{hay!;46pz^BPAU z7t@iS=M#LbCHzU^J0tu00${TM#d&zc*mO!EqX@LvNT1dK37<{B%fRnU9$gZ(>9{l_H=PqaNJ~~dK zkJny#NZ$9w16-_+XIb)4Rd<#~&rUw~%QfdB!|y~7amQ-kpI+l5U8IyGZ~f)j@14@$ zN)1nN{x=>-E%kv;!beEq<7pq+IQW^wKQkX(`fq!H6GEX|2(jP)Pw7EIFT(r(-==5b zF2En?3HmGg3;HVkDg7aRo=!tD1|)z4kN^@u0!RP}AOR$R1dsp{KmthM|3QG=E8wVo z%Yci;>@!icqiEEQA`v?ZhwUg7vZG+ojsgKY^84+mrNxc}!H#&|j(k1=ZUNZ)|Lise z1|)z4kN^@u0!RP}AOR$R1dsp{KmthM<|V-X{~y=?H*fu7$&dgNKmter2_OL^fCP{L t5 0) { - no_rows.hide(); - } - table.show(); - - } - else { - // Filter table items by value. - var hidden = 0; - var shown = 0; - - // Hide / show elements. - $.each(table_row_names, function () { - var element = $(this).parents("tr"); - - if ($(this).text().indexOf(filter_value) === -1) { - // hide - element.addClass("hidden"); - hidden++; - } - else { - // show - element.removeClass("hidden"); - shown++; - } - }); - - // Show placeholder if no rows will be displayed. - if (no_rows.length > 0) { - if (shown === 0) { - // Show placeholder, hide table. - no_rows.show(); - table.hide(); - } - else { - // Hide placeholder, show table. - no_rows.hide(); - table.show(); - } - } - - // Manage dynamic header: - if (hidden > 0) { - // Calculate new dynamic sum values based on visible rows. - for (var column = 2; column < 20; column++) { - // Calculate summed value. - var cells = table_rows.find('td:nth-child(' + column + ')'); - if (!cells.length) { - // No more columns...! - break; - } - - var sum = 0, numer = 0, denom = 0; - $.each(cells.filter(':visible'), function () { - var ratio = $(this).data("ratio"); - if (ratio) { - var splitted = ratio.split(" "); - numer += parseInt(splitted[0], 10); - denom += parseInt(splitted[1], 10); - } - else { - sum += parseInt(this.innerHTML, 10); - } - }); - - // Get footer cell element. - var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')'); - - // Set value into dynamic footer cell element. - if (cells[0].innerHTML.indexOf('%') > -1) { - // Percentage columns use the numerator and denominator, - // and adapt to the number of decimal places. - var match = /\.([0-9]+)/.exec(cells[0].innerHTML); - var places = 0; - if (match) { - places = match[1].length; - } - var pct = numer * 100 / denom; - footer_cell.text(pct.toFixed(places) + '%'); - } - else { - footer_cell.text(sum); - } - } - - // Hide standard footer, show dynamic footer. - table_footer.addClass("hidden"); - table_dynamic_footer.removeClass("hidden"); - } - else { - // Show standard footer, hide dynamic footer. - table_footer.removeClass("hidden"); - table_dynamic_footer.addClass("hidden"); - } - } - })); - - // Trigger change event on setup, to force filter on page refresh - // (filter value may still be present). - $("#filter").trigger("change"); -}; - -// Loaded on index.html -coverage.index_ready = function ($) { - // Look for a localStorage item containing previous sort settings: - var sort_list = []; - var storage_name = "COVERAGE_INDEX_SORT"; - var stored_list = undefined; - try { - stored_list = localStorage.getItem(storage_name); - } catch(err) {} - - if (stored_list) { - sort_list = JSON.parse('[[' + stored_list + ']]'); - } - - // Create a new widget which exists only to save and restore - // the sort order: - $.tablesorter.addWidget({ - id: "persistentSort", - - // Format is called by the widget before displaying: - format: function (table) { - if (table.config.sortList.length === 0 && sort_list.length > 0) { - // This table hasn't been sorted before - we'll use - // our stored settings: - $(table).trigger('sorton', [sort_list]); - } - else { - // This is not the first load - something has - // already defined sorting so we'll just update - // our stored value to match: - sort_list = table.config.sortList; - } - } - }); - - // Configure our tablesorter to handle the variable number of - // columns produced depending on report options: - var headers = []; - var col_count = $("table.index > thead > tr > th").length; - - headers[0] = { sorter: 'text' }; - for (i = 1; i < col_count-1; i++) { - headers[i] = { sorter: 'digit' }; - } - headers[col_count-1] = { sorter: 'percent' }; - - // Enable the table sorter: - $("table.index").tablesorter({ - widgets: ['persistentSort'], - headers: headers - }); - - coverage.assign_shortkeys(); - coverage.wire_up_help_panel(); - coverage.wire_up_filter(); - - // Watch for page unload events so we can save the final sort settings: - $(window).on("unload", function () { - try { - localStorage.setItem(storage_name, sort_list.toString()) - } catch(err) {} - }); -}; - -// -- pyfile stuff -- - -coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; - -coverage.pyfile_ready = function ($) { - // If we're directed to a particular line number, highlight the line. - var frag = location.hash; - if (frag.length > 2 && frag[1] === 't') { - $(frag).addClass('highlight'); - coverage.set_sel(parseInt(frag.substr(2), 10)); - } - else { - coverage.set_sel(0); - } - - $(document) - .bind('keydown', 'j', coverage.to_next_chunk_nicely) - .bind('keydown', 'k', coverage.to_prev_chunk_nicely) - .bind('keydown', '0', coverage.to_top) - .bind('keydown', '1', coverage.to_first_chunk) - ; - - $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");}); - $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");}); - $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); - $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); - - coverage.filters = undefined; - try { - coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); - } catch(err) {} - - if (coverage.filters) { - coverage.filters = JSON.parse(coverage.filters); - } - else { - coverage.filters = {run: false, exc: true, mis: true, par: true}; - } - - for (cls in coverage.filters) { - coverage.set_line_visibilty(cls, coverage.filters[cls]); - } - - coverage.assign_shortkeys(); - coverage.wire_up_help_panel(); - - coverage.init_scroll_markers(); - - // Rebuild scroll markers when the window height changes. - $(window).resize(coverage.build_scroll_markers); -}; - -coverage.toggle_lines = function (btn, cls) { - var onoff = !$(btn).hasClass("show_" + cls); - coverage.set_line_visibilty(cls, onoff); - coverage.build_scroll_markers(); - coverage.filters[cls] = onoff; - try { - localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); - } catch(err) {} -}; - -coverage.set_line_visibilty = function (cls, onoff) { - var show = "show_" + cls; - var btn = $(".button_toggle_" + cls); - if (onoff) { - $("#source ." + cls).addClass(show); - btn.addClass(show); - } - else { - $("#source ." + cls).removeClass(show); - btn.removeClass(show); - } -}; - -// Return the nth line div. -coverage.line_elt = function (n) { - return $("#t" + n); -}; - -// Return the nth line number div. -coverage.num_elt = function (n) { - return $("#n" + n); -}; - -// Set the selection. b and e are line numbers. -coverage.set_sel = function (b, e) { - // The first line selected. - coverage.sel_begin = b; - // The next line not selected. - coverage.sel_end = (e === undefined) ? b+1 : e; -}; - -coverage.to_top = function () { - coverage.set_sel(0, 1); - coverage.scroll_window(0); -}; - -coverage.to_first_chunk = function () { - coverage.set_sel(0, 1); - coverage.to_next_chunk(); -}; - -// Return a string indicating what kind of chunk this line belongs to, -// or null if not a chunk. -coverage.chunk_indicator = function (line_elt) { - var klass = line_elt.attr('class'); - if (klass) { - var m = klass.match(/\bshow_\w+\b/); - if (m) { - return m[0]; - } - } - return null; -}; - -coverage.to_next_chunk = function () { - var c = coverage; - - // Find the start of the next colored chunk. - var probe = c.sel_end; - var chunk_indicator, probe_line; - while (true) { - probe_line = c.line_elt(probe); - if (probe_line.length === 0) { - return; - } - chunk_indicator = c.chunk_indicator(probe_line); - if (chunk_indicator) { - break; - } - probe++; - } - - // There's a next chunk, `probe` points to it. - var begin = probe; - - // Find the end of this chunk. - var next_indicator = chunk_indicator; - while (next_indicator === chunk_indicator) { - probe++; - probe_line = c.line_elt(probe); - next_indicator = c.chunk_indicator(probe_line); - } - c.set_sel(begin, probe); - c.show_selection(); -}; - -coverage.to_prev_chunk = function () { - var c = coverage; - - // Find the end of the prev colored chunk. - var probe = c.sel_begin-1; - var probe_line = c.line_elt(probe); - if (probe_line.length === 0) { - return; - } - var chunk_indicator = c.chunk_indicator(probe_line); - while (probe > 0 && !chunk_indicator) { - probe--; - probe_line = c.line_elt(probe); - if (probe_line.length === 0) { - return; - } - chunk_indicator = c.chunk_indicator(probe_line); - } - - // There's a prev chunk, `probe` points to its last line. - var end = probe+1; - - // Find the beginning of this chunk. - var prev_indicator = chunk_indicator; - while (prev_indicator === chunk_indicator) { - probe--; - probe_line = c.line_elt(probe); - prev_indicator = c.chunk_indicator(probe_line); - } - c.set_sel(probe+1, end); - c.show_selection(); -}; - -// Return the line number of the line nearest pixel position pos -coverage.line_at_pos = function (pos) { - var l1 = coverage.line_elt(1), - l2 = coverage.line_elt(2), - result; - if (l1.length && l2.length) { - var l1_top = l1.offset().top, - line_height = l2.offset().top - l1_top, - nlines = (pos - l1_top) / line_height; - if (nlines < 1) { - result = 1; - } - else { - result = Math.ceil(nlines); - } - } - else { - result = 1; - } - return result; -}; - -// Returns 0, 1, or 2: how many of the two ends of the selection are on -// the screen right now? -coverage.selection_ends_on_screen = function () { - if (coverage.sel_begin === 0) { - return 0; - } - - var top = coverage.line_elt(coverage.sel_begin); - var next = coverage.line_elt(coverage.sel_end-1); - - return ( - (top.isOnScreen() ? 1 : 0) + - (next.isOnScreen() ? 1 : 0) - ); -}; - -coverage.to_next_chunk_nicely = function () { - coverage.finish_scrolling(); - if (coverage.selection_ends_on_screen() === 0) { - // The selection is entirely off the screen: select the top line on - // the screen. - var win = $(window); - coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); - } - coverage.to_next_chunk(); -}; - -coverage.to_prev_chunk_nicely = function () { - coverage.finish_scrolling(); - if (coverage.selection_ends_on_screen() === 0) { - var win = $(window); - coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); - } - coverage.to_prev_chunk(); -}; - -// Select line number lineno, or if it is in a colored chunk, select the -// entire chunk -coverage.select_line_or_chunk = function (lineno) { - var c = coverage; - var probe_line = c.line_elt(lineno); - if (probe_line.length === 0) { - return; - } - var the_indicator = c.chunk_indicator(probe_line); - if (the_indicator) { - // The line is in a highlighted chunk. - // Search backward for the first line. - var probe = lineno; - var indicator = the_indicator; - while (probe > 0 && indicator === the_indicator) { - probe--; - probe_line = c.line_elt(probe); - if (probe_line.length === 0) { - break; - } - indicator = c.chunk_indicator(probe_line); - } - var begin = probe + 1; - - // Search forward for the last line. - probe = lineno; - indicator = the_indicator; - while (indicator === the_indicator) { - probe++; - probe_line = c.line_elt(probe); - indicator = c.chunk_indicator(probe_line); - } - - coverage.set_sel(begin, probe); - } - else { - coverage.set_sel(lineno); - } -}; - -coverage.show_selection = function () { - var c = coverage; - - // Highlight the lines in the chunk - $(".linenos .highlight").removeClass("highlight"); - for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { - c.num_elt(probe).addClass("highlight"); - } - - c.scroll_to_selection(); -}; - -coverage.scroll_to_selection = function () { - // Scroll the page if the chunk isn't fully visible. - if (coverage.selection_ends_on_screen() < 2) { - // Need to move the page. The html,body trick makes it scroll in all - // browsers, got it from http://stackoverflow.com/questions/3042651 - var top = coverage.line_elt(coverage.sel_begin); - var top_pos = parseInt(top.offset().top, 10); - coverage.scroll_window(top_pos - 30); - } -}; - -coverage.scroll_window = function (to_pos) { - $("html,body").animate({scrollTop: to_pos}, 200); -}; - -coverage.finish_scrolling = function () { - $("html,body").stop(true, true); -}; - -coverage.init_scroll_markers = function () { - var c = coverage; - // Init some variables - c.lines_len = $('#source p').length; - c.body_h = $('body').height(); - c.header_h = $('div#header').height(); - - // Build html - c.build_scroll_markers(); -}; - -coverage.build_scroll_markers = function () { - var c = coverage, - min_line_height = 3, - max_line_height = 10, - visible_window_h = $(window).height(); - - c.lines_to_mark = $('#source').find('p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par'); - $('#scroll_marker').remove(); - // Don't build markers if the window has no scroll bar. - if (c.body_h <= visible_window_h) { - return; - } - - $("body").append("
 
"); - var scroll_marker = $('#scroll_marker'), - marker_scale = scroll_marker.height() / c.body_h, - line_height = scroll_marker.height() / c.lines_len; - - // Line height must be between the extremes. - if (line_height > min_line_height) { - if (line_height > max_line_height) { - line_height = max_line_height; - } - } - else { - line_height = min_line_height; - } - - var previous_line = -99, - last_mark, - last_top, - offsets = {}; - - // Calculate line offsets outside loop to prevent relayouts - c.lines_to_mark.each(function() { - offsets[this.id] = $(this).offset().top; - }); - c.lines_to_mark.each(function () { - var id_name = $(this).attr('id'), - line_top = Math.round(offsets[id_name] * marker_scale), - line_number = parseInt(id_name.substring(1, id_name.length)); - - if (line_number === previous_line + 1) { - // If this solid missed block just make previous mark higher. - last_mark.css({ - 'height': line_top + line_height - last_top - }); - } - else { - // Add colored line in scroll_marker block. - scroll_marker.append('
'); - last_mark = $('#m' + line_number); - last_mark.css({ - 'height': line_height, - 'top': line_top - }); - last_top = line_top; - } - - previous_line = line_number; - }); -}; diff --git a/htmlcov/favicon_32.png b/htmlcov/favicon_32.png deleted file mode 100644 index 8649f0475d8d20793b2ec431fe25a186a414cf10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1732 zcmV;#20QtQP)K2KOkBOVxIZChq#W-v7@TU%U6P(wycKT1hUJUToW3ke1U1ONa4 z000000000000000bb)GRa9mqwR9|UWHy;^RUrt?IT__Y0JUcxmBP0(51q1>E00030 z|NrOz)aw7%8sJzM<5^g%z7^qE`}_Ot|JUUG(NUkWzR|7K?Zo%@_v-8G-1N%N=D$;; zw;keH4dGY$`1t4M=HK_s*zm^0#KgqfwWhe3qO_HtvXYvtjgX>;-~C$L`&k>^R)9)7 zdPh2TL^pCnHC#0+_4D)M`p?qp!pq{jO_{8;$fbaflbx`Tn52n|n}8VFRTA1&ugOP< zPd{uvFjz7t*Vot1&d$l-xWCk}s;sQL&#O(Bskh6gqNJv>#iB=ypG1e3K!K4yc7!~M zfj4S*g^zZ7eP$+_Sl07Z646l;%urinP#D8a6TwRtnLIRcI!r4f@bK~9-`~;E(N?Lv zSEst7s;rcxsi~}{Nsytfz@MtUoR*iFc8!#vvx}Umhm4blk(_~MdVD-@dW&>!Nn~ro z_E~-ESVQAj6Wmn;(olz(O&_{U2*pZBc1aYjMh>Dq3z|6`jW`RDHV=t3I6yRKJ~LOX zz_z!!vbVXPqob#=pj3^VMT?x6t(irRmSKsMo1~LLkB&=#j!=M%NP35mfqim$drWb9 zYIb>no_LUwc!r^NkDzs4YHu@=ZHRzrafWDZd1EhEVq=tGX?tK$pIa)DTh#bkvh!J- z?^%@YS!U*0E8$q$_*aOTQ&)Ra64g>ep;BdcQgvlg8qQHrP*E$;P{-m=A*@axn@$bO zO-Y4JzS&EAi%YG}N?cn?YFS7ivPY=EMV6~YH;+Xxu|tefLS|Aza)Cg6us#)=JW!uH zQa?H>d^j+YHCtyjL^LulF*05|F$RG!AX_OHVI&MtA~_@=5_lU|0000rbW%=J06GH4 z^5LD8b8apw8vNh1ua1mF{{Hy)_U`NA;Nacc+sCpuHXa-V{r&yz?c(9#+}oX+NmiRW z+W-IqK1oDDR5;6GfCDCOP5}iL5fK(cB~ET81`MFgF2kGa9AjhSIk~-E-4&*tPPKdiilQJ11k_J082ZS z>@TvivP!5ZFG?t@{t+GpR3XR&@*hA_VE1|Lo8@L@)l*h(Z@=?c-NS$Fk&&61IzUU9 z*nPqBM=OBZ-6ka1SJgGAS-Us5EN)r#dUX%>wQZLa2ytPCtMKp)Ob z*xcu38Z&d5<-NBS)@jRD+*!W*cf-m_wmxDEqBf?czI%3U0J$Xik;lA`jg}VH?(S(V zE!M3;X2B8w0TnnW&6(8;_Uc)WD;Ms6PKP+s(sFgO!}B!^ES~GDt4qLPxwYB)^7)XA zZwo9zDy-B0B+jT6V=!=bo(zs_8{eBA78gT9GH$(DVhz;4VAYwz+bOIdZ-PNb|I&rl z^XG=vFLF)1{&nT2*0vMz#}7^9hXzzf&ZdKlEj{LihP;|;Ywqn35ajP?H?7t|i-Un% z&&kxee@9B{nwgv1+S-~0)E1{ob1^Wn`F2isurqThKK=3%&;`@{0{!D- z&CSj80t;uPu&FaJFtSXKH#ajgGj}=sEad7US6jP0|Db@0j)?(5@sf<7`~a9>s;wCa zm^)spe{uxGFmrJYI9cOh7s$>8Npkt-5EWB1UKc`{W{y5Ce$1+nM9Cr;);=Ju#N^62OSlJMn7omiUgP&ErsYzT~iGxcW aE(`!K@+CXylaC4j0000 - - - - Coverage report - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- n - s - m - x - c   change column sorting -

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Modulestatementsmissingexcludedcoverage
Total1481507066%
sygnal/__init__.py500100%
sygnal/apnspushkin.py23676068%
sygnal/apnstruncate.py655092%
sygnal/exceptions.py1300100%
sygnal/gcmpushkin.py19649075%
sygnal/helper/__init__.py000100%
sygnal/helper/context_factory.py6534048%
sygnal/helper/proxy/__init__.py1300100%
sygnal/helper/proxy/connectproxyclient_twisted.py9915085%
sygnal/helper/proxy/proxy_asyncio.py11926078%
sygnal/helper/proxy/proxyagent_twisted.py5310081%
sygnal/http.py16628083%
sygnal/notifications.py979091%
sygnal/sygnal.py16782051%
sygnal/utils.py239061%
sygnal/webpushpushkin.py16416400%
-

- No items found using the specified filter. -

-
- - - diff --git a/htmlcov/jquery.ba-throttle-debounce.min.js b/htmlcov/jquery.ba-throttle-debounce.min.js deleted file mode 100644 index 648fe5d3..00000000 --- a/htmlcov/jquery.ba-throttle-debounce.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * jQuery throttle / debounce - v1.1 - 3/7/2010 - * http://benalman.com/projects/jquery-throttle-debounce-plugin/ - * - * Copyright (c) 2010 "Cowboy" Ben Alman - * Dual licensed under the MIT and GPL licenses. - * http://benalman.com/about/license/ - */ -(function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this); diff --git a/htmlcov/jquery.hotkeys.js b/htmlcov/jquery.hotkeys.js deleted file mode 100644 index 09b21e03..00000000 --- a/htmlcov/jquery.hotkeys.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * jQuery Hotkeys Plugin - * Copyright 2010, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * - * Based upon the plugin by Tzury Bar Yochay: - * http://github.com/tzuryby/hotkeys - * - * Original idea by: - * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ -*/ - -(function(jQuery){ - - jQuery.hotkeys = { - version: "0.8", - - specialKeys: { - 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", - 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", - 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", - 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", - 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", - 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", - 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" - }, - - shiftNums: { - "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", - "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", - ".": ">", "/": "?", "\\": "|" - } - }; - - function keyHandler( handleObj ) { - // Only care when a possible input has been specified - if ( typeof handleObj.data !== "string" ) { - return; - } - - var origHandler = handleObj.handler, - keys = handleObj.data.toLowerCase().split(" "); - - handleObj.handler = function( event ) { - // Don't fire in text-accepting inputs that we didn't directly bind to - if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || - event.target.type === "text") ) { - return; - } - - // Keypress represents characters, not special keys - var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], - character = String.fromCharCode( event.which ).toLowerCase(), - key, modif = "", possible = {}; - - // check combinations (alt|ctrl|shift+anything) - if ( event.altKey && special !== "alt" ) { - modif += "alt+"; - } - - if ( event.ctrlKey && special !== "ctrl" ) { - modif += "ctrl+"; - } - - // TODO: Need to make sure this works consistently across platforms - if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { - modif += "meta+"; - } - - if ( event.shiftKey && special !== "shift" ) { - modif += "shift+"; - } - - if ( special ) { - possible[ modif + special ] = true; - - } else { - possible[ modif + character ] = true; - possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; - - // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" - if ( modif === "shift+" ) { - possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; - } - } - - for ( var i = 0, l = keys.length; i < l; i++ ) { - if ( possible[ keys[i] ] ) { - return origHandler.apply( this, arguments ); - } - } - }; - } - - jQuery.each([ "keydown", "keyup", "keypress" ], function() { - jQuery.event.special[ this ] = { add: keyHandler }; - }); - -})( jQuery ); diff --git a/htmlcov/jquery.isonscreen.js b/htmlcov/jquery.isonscreen.js deleted file mode 100644 index 0182ebd2..00000000 --- a/htmlcov/jquery.isonscreen.js +++ /dev/null @@ -1,53 +0,0 @@ -/* Copyright (c) 2010 - * @author Laurence Wheway - * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) - * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. - * - * @version 1.2.0 - */ -(function($) { - jQuery.extend({ - isOnScreen: function(box, container) { - //ensure numbers come in as intgers (not strings) and remove 'px' is it's there - for(var i in box){box[i] = parseFloat(box[i])}; - for(var i in container){container[i] = parseFloat(container[i])}; - - if(!container){ - container = { - left: $(window).scrollLeft(), - top: $(window).scrollTop(), - width: $(window).width(), - height: $(window).height() - } - } - - if( box.left+box.width-container.left > 0 && - box.left < container.width+container.left && - box.top+box.height-container.top > 0 && - box.top < container.height+container.top - ) return true; - return false; - } - }) - - - jQuery.fn.isOnScreen = function (container) { - for(var i in container){container[i] = parseFloat(container[i])}; - - if(!container){ - container = { - left: $(window).scrollLeft(), - top: $(window).scrollTop(), - width: $(window).width(), - height: $(window).height() - } - } - - if( $(this).offset().left+$(this).width()-container.left > 0 && - $(this).offset().left < container.width+container.left && - $(this).offset().top+$(this).height()-container.top > 0 && - $(this).offset().top < container.height+container.top - ) return true; - return false; - } -})(jQuery); diff --git a/htmlcov/jquery.min.js b/htmlcov/jquery.min.js deleted file mode 100644 index 0363dfea..00000000 --- a/htmlcov/jquery.min.js +++ /dev/null @@ -1,5 +0,0 @@ -(function(global,factory){if(typeof module==="object"&&typeof module.exports==="object"){module.exports=global.document?factory(global,true):function(w){if(!w.document){throw new Error("jQuery requires a window with a document")}return factory(w)}}else{factory(global)}})(typeof window!=="undefined"?window:this,function(window,noGlobal){var deletedIds=[];var slice=deletedIds.slice;var concat=deletedIds.concat;var push=deletedIds.push;var indexOf=deletedIds.indexOf;var class2type={};var toString=class2type.toString;var hasOwn=class2type.hasOwnProperty;var support={};var version="1.11.3",jQuery=function(selector,context){return new jQuery.fn.init(selector,context)},rtrim=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,rmsPrefix=/^-ms-/,rdashAlpha=/-([\da-z])/gi,fcamelCase=function(all,letter){return letter.toUpperCase()};jQuery.fn=jQuery.prototype={jquery:version,constructor:jQuery,selector:"",length:0,toArray:function(){return slice.call(this)},get:function(num){return num!=null?num<0?this[num+this.length]:this[num]:slice.call(this)},pushStack:function(elems){var ret=jQuery.merge(this.constructor(),elems);ret.prevObject=this;ret.context=this.context;return ret},each:function(callback,args){return jQuery.each(this,callback,args)},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem)}))},slice:function(){return this.pushStack(slice.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(i){var len=this.length,j=+i+(i<0?len:0);return this.pushStack(j>=0&&j=0},isEmptyObject:function(obj){var name;for(name in obj){return false}return true},isPlainObject:function(obj){var key;if(!obj||jQuery.type(obj)!=="object"||obj.nodeType||jQuery.isWindow(obj)){return false}try{if(obj.constructor&&!hasOwn.call(obj,"constructor")&&!hasOwn.call(obj.constructor.prototype,"isPrototypeOf")){return false}}catch(e){return false}if(support.ownLast){for(key in obj){return hasOwn.call(obj,key)}}for(key in obj){}return key===undefined||hasOwn.call(obj,key)},type:function(obj){if(obj==null){return obj+""}return typeof obj==="object"||typeof obj==="function"?class2type[toString.call(obj)]||"object":typeof obj},globalEval:function(data){if(data&&jQuery.trim(data)){(window.execScript||function(data){window["eval"].call(window,data)})(data)}},camelCase:function(string){return string.replace(rmsPrefix,"ms-").replace(rdashAlpha,fcamelCase)},nodeName:function(elem,name){return elem.nodeName&&elem.nodeName.toLowerCase()===name.toLowerCase()},each:function(obj,callback,args){var value,i=0,length=obj.length,isArray=isArraylike(obj);if(args){if(isArray){for(;i0&&length-1 in obj}var Sizzle=function(window){var i,support,Expr,getText,isXML,tokenize,compile,select,outermostContext,sortInput,hasDuplicate,setDocument,document,docElem,documentIsHTML,rbuggyQSA,rbuggyMatches,matches,contains,expando="sizzle"+1*new Date,preferredDoc=window.document,dirruns=0,done=0,classCache=createCache(),tokenCache=createCache(),compilerCache=createCache(),sortOrder=function(a,b){if(a===b){hasDuplicate=true}return 0},MAX_NEGATIVE=1<<31,hasOwn={}.hasOwnProperty,arr=[],pop=arr.pop,push_native=arr.push,push=arr.push,slice=arr.slice,indexOf=function(list,elem){var i=0,len=list.length;for(;i+~]|"+whitespace+")"+whitespace+"*"),rattributeQuotes=new RegExp("="+whitespace+"*([^\\]'\"]*?)"+whitespace+"*\\]","g"),rpseudo=new RegExp(pseudos),ridentifier=new RegExp("^"+identifier+"$"),matchExpr={ID:new RegExp("^#("+characterEncoding+")"),CLASS:new RegExp("^\\.("+characterEncoding+")"),TAG:new RegExp("^("+characterEncoding.replace("w","w*")+")"),ATTR:new RegExp("^"+attributes),PSEUDO:new RegExp("^"+pseudos),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+whitespace+"*(even|odd|(([+-]|)(\\d*)n|)"+whitespace+"*(?:([+-]|)"+whitespace+"*(\\d+)|))"+whitespace+"*\\)|)","i"),bool:new RegExp("^(?:"+booleans+")$","i"),needsContext:new RegExp("^"+whitespace+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+whitespace+"*((?:-\\d)?\\d*)"+whitespace+"*\\)|)(?=[^-]|$)","i")},rinputs=/^(?:input|select|textarea|button)$/i,rheader=/^h\d$/i,rnative=/^[^{]+\{\s*\[native \w/,rquickExpr=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,rsibling=/[+~]/,rescape=/'|\\/g,runescape=new RegExp("\\\\([\\da-f]{1,6}"+whitespace+"?|("+whitespace+")|.)","ig"),funescape=function(_,escaped,escapedWhitespace){var high="0x"+escaped-65536;return high!==high||escapedWhitespace?escaped:high<0?String.fromCharCode(high+65536):String.fromCharCode(high>>10|55296,high&1023|56320)},unloadHandler=function(){setDocument()};try{push.apply(arr=slice.call(preferredDoc.childNodes),preferredDoc.childNodes);arr[preferredDoc.childNodes.length].nodeType}catch(e){push={apply:arr.length?function(target,els){push_native.apply(target,slice.call(els))}:function(target,els){var j=target.length,i=0;while(target[j++]=els[i++]){}target.length=j-1}}}function Sizzle(selector,context,results,seed){var match,elem,m,nodeType,i,groups,old,nid,newContext,newSelector;if((context?context.ownerDocument||context:preferredDoc)!==document){setDocument(context)}context=context||document;results=results||[];nodeType=context.nodeType;if(typeof selector!=="string"||!selector||nodeType!==1&&nodeType!==9&&nodeType!==11){return results}if(!seed&&documentIsHTML){if(nodeType!==11&&(match=rquickExpr.exec(selector))){if(m=match[1]){if(nodeType===9){elem=context.getElementById(m);if(elem&&elem.parentNode){if(elem.id===m){results.push(elem);return results}}else{return results}}else{if(context.ownerDocument&&(elem=context.ownerDocument.getElementById(m))&&contains(context,elem)&&elem.id===m){results.push(elem);return results}}}else if(match[2]){push.apply(results,context.getElementsByTagName(selector));return results}else if((m=match[3])&&support.getElementsByClassName){push.apply(results,context.getElementsByClassName(m));return results}}if(support.qsa&&(!rbuggyQSA||!rbuggyQSA.test(selector))){nid=old=expando;newContext=context;newSelector=nodeType!==1&&selector;if(nodeType===1&&context.nodeName.toLowerCase()!=="object"){groups=tokenize(selector);if(old=context.getAttribute("id")){nid=old.replace(rescape,"\\$&")}else{context.setAttribute("id",nid)}nid="[id='"+nid+"'] ";i=groups.length;while(i--){groups[i]=nid+toSelector(groups[i])}newContext=rsibling.test(selector)&&testContext(context.parentNode)||context;newSelector=groups.join(",")}if(newSelector){try{push.apply(results,newContext.querySelectorAll(newSelector));return results}catch(qsaError){}finally{if(!old){context.removeAttribute("id")}}}}}return select(selector.replace(rtrim,"$1"),context,results,seed)}function createCache(){var keys=[];function cache(key,value){if(keys.push(key+" ")>Expr.cacheLength){delete cache[keys.shift()]}return cache[key+" "]=value}return cache}function markFunction(fn){fn[expando]=true;return fn}function assert(fn){var div=document.createElement("div");try{return!!fn(div)}catch(e){return false}finally{if(div.parentNode){div.parentNode.removeChild(div)}div=null}}function addHandle(attrs,handler){var arr=attrs.split("|"),i=attrs.length;while(i--){Expr.attrHandle[arr[i]]=handler}}function siblingCheck(a,b){var cur=b&&a,diff=cur&&a.nodeType===1&&b.nodeType===1&&(~b.sourceIndex||MAX_NEGATIVE)-(~a.sourceIndex||MAX_NEGATIVE);if(diff){return diff}if(cur){while(cur=cur.nextSibling){if(cur===b){return-1}}}return a?1:-1}function createInputPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type===type}}function createButtonPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return(name==="input"||name==="button")&&elem.type===type}}function createPositionalPseudo(fn){return markFunction(function(argument){argument=+argument;return markFunction(function(seed,matches){var j,matchIndexes=fn([],seed.length,argument),i=matchIndexes.length;while(i--){if(seed[j=matchIndexes[i]]){seed[j]=!(matches[j]=seed[j])}}})})}function testContext(context){return context&&typeof context.getElementsByTagName!=="undefined"&&context}support=Sizzle.support={};isXML=Sizzle.isXML=function(elem){var documentElement=elem&&(elem.ownerDocument||elem).documentElement;return documentElement?documentElement.nodeName!=="HTML":false};setDocument=Sizzle.setDocument=function(node){var hasCompare,parent,doc=node?node.ownerDocument||node:preferredDoc;if(doc===document||doc.nodeType!==9||!doc.documentElement){return document}document=doc;docElem=doc.documentElement;parent=doc.defaultView;if(parent&&parent!==parent.top){if(parent.addEventListener){parent.addEventListener("unload",unloadHandler,false)}else if(parent.attachEvent){parent.attachEvent("onunload",unloadHandler)}}documentIsHTML=!isXML(doc);support.attributes=assert(function(div){div.className="i";return!div.getAttribute("className")});support.getElementsByTagName=assert(function(div){div.appendChild(doc.createComment(""));return!div.getElementsByTagName("*").length});support.getElementsByClassName=rnative.test(doc.getElementsByClassName);support.getById=assert(function(div){docElem.appendChild(div).id=expando;return!doc.getElementsByName||!doc.getElementsByName(expando).length});if(support.getById){Expr.find["ID"]=function(id,context){if(typeof context.getElementById!=="undefined"&&documentIsHTML){var m=context.getElementById(id);return m&&m.parentNode?[m]:[]}};Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){return elem.getAttribute("id")===attrId}}}else{delete Expr.find["ID"];Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){var node=typeof elem.getAttributeNode!=="undefined"&&elem.getAttributeNode("id");return node&&node.value===attrId}}}Expr.find["TAG"]=support.getElementsByTagName?function(tag,context){if(typeof context.getElementsByTagName!=="undefined"){return context.getElementsByTagName(tag)}else if(support.qsa){return context.querySelectorAll(tag)}}:function(tag,context){var elem,tmp=[],i=0,results=context.getElementsByTagName(tag);if(tag==="*"){while(elem=results[i++]){if(elem.nodeType===1){tmp.push(elem)}}return tmp}return results};Expr.find["CLASS"]=support.getElementsByClassName&&function(className,context){if(documentIsHTML){return context.getElementsByClassName(className)}};rbuggyMatches=[];rbuggyQSA=[];if(support.qsa=rnative.test(doc.querySelectorAll)){assert(function(div){docElem.appendChild(div).innerHTML=""+"";if(div.querySelectorAll("[msallowcapture^='']").length){rbuggyQSA.push("[*^$]="+whitespace+"*(?:''|\"\")")}if(!div.querySelectorAll("[selected]").length){rbuggyQSA.push("\\["+whitespace+"*(?:value|"+booleans+")")}if(!div.querySelectorAll("[id~="+expando+"-]").length){rbuggyQSA.push("~=")}if(!div.querySelectorAll(":checked").length){rbuggyQSA.push(":checked")}if(!div.querySelectorAll("a#"+expando+"+*").length){rbuggyQSA.push(".#.+[+~]")}});assert(function(div){var input=doc.createElement("input");input.setAttribute("type","hidden");div.appendChild(input).setAttribute("name","D");if(div.querySelectorAll("[name=d]").length){rbuggyQSA.push("name"+whitespace+"*[*^$|!~]?=")}if(!div.querySelectorAll(":enabled").length){rbuggyQSA.push(":enabled",":disabled")}div.querySelectorAll("*,:x");rbuggyQSA.push(",.*:")})}if(support.matchesSelector=rnative.test(matches=docElem.matches||docElem.webkitMatchesSelector||docElem.mozMatchesSelector||docElem.oMatchesSelector||docElem.msMatchesSelector)){assert(function(div){support.disconnectedMatch=matches.call(div,"div");matches.call(div,"[s!='']:x");rbuggyMatches.push("!=",pseudos)})}rbuggyQSA=rbuggyQSA.length&&new RegExp(rbuggyQSA.join("|"));rbuggyMatches=rbuggyMatches.length&&new RegExp(rbuggyMatches.join("|"));hasCompare=rnative.test(docElem.compareDocumentPosition);contains=hasCompare||rnative.test(docElem.contains)?function(a,b){var adown=a.nodeType===9?a.documentElement:a,bup=b&&b.parentNode;return a===bup||!!(bup&&bup.nodeType===1&&(adown.contains?adown.contains(bup):a.compareDocumentPosition&&a.compareDocumentPosition(bup)&16))}:function(a,b){if(b){while(b=b.parentNode){if(b===a){return true}}}return false};sortOrder=hasCompare?function(a,b){if(a===b){hasDuplicate=true;return 0}var compare=!a.compareDocumentPosition-!b.compareDocumentPosition;if(compare){return compare}compare=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1;if(compare&1||!support.sortDetached&&b.compareDocumentPosition(a)===compare){if(a===doc||a.ownerDocument===preferredDoc&&contains(preferredDoc,a)){return-1}if(b===doc||b.ownerDocument===preferredDoc&&contains(preferredDoc,b)){return 1}return sortInput?indexOf(sortInput,a)-indexOf(sortInput,b):0}return compare&4?-1:1}:function(a,b){if(a===b){hasDuplicate=true;return 0}var cur,i=0,aup=a.parentNode,bup=b.parentNode,ap=[a],bp=[b];if(!aup||!bup){return a===doc?-1:b===doc?1:aup?-1:bup?1:sortInput?indexOf(sortInput,a)-indexOf(sortInput,b):0}else if(aup===bup){return siblingCheck(a,b)}cur=a;while(cur=cur.parentNode){ap.unshift(cur)}cur=b;while(cur=cur.parentNode){bp.unshift(cur)}while(ap[i]===bp[i]){i++}return i?siblingCheck(ap[i],bp[i]):ap[i]===preferredDoc?-1:bp[i]===preferredDoc?1:0};return doc};Sizzle.matches=function(expr,elements){return Sizzle(expr,null,null,elements)};Sizzle.matchesSelector=function(elem,expr){if((elem.ownerDocument||elem)!==document){setDocument(elem)}expr=expr.replace(rattributeQuotes,"='$1']");if(support.matchesSelector&&documentIsHTML&&(!rbuggyMatches||!rbuggyMatches.test(expr))&&(!rbuggyQSA||!rbuggyQSA.test(expr))){try{var ret=matches.call(elem,expr);if(ret||support.disconnectedMatch||elem.document&&elem.document.nodeType!==11){return ret}}catch(e){}}return Sizzle(expr,document,null,[elem]).length>0};Sizzle.contains=function(context,elem){if((context.ownerDocument||context)!==document){setDocument(context)}return contains(context,elem)};Sizzle.attr=function(elem,name){if((elem.ownerDocument||elem)!==document){setDocument(elem)}var fn=Expr.attrHandle[name.toLowerCase()],val=fn&&hasOwn.call(Expr.attrHandle,name.toLowerCase())?fn(elem,name,!documentIsHTML):undefined;return val!==undefined?val:support.attributes||!documentIsHTML?elem.getAttribute(name):(val=elem.getAttributeNode(name))&&val.specified?val.value:null};Sizzle.error=function(msg){throw new Error("Syntax error, unrecognized expression: "+msg)};Sizzle.uniqueSort=function(results){var elem,duplicates=[],j=0,i=0;hasDuplicate=!support.detectDuplicates;sortInput=!support.sortStable&&results.slice(0);results.sort(sortOrder);if(hasDuplicate){while(elem=results[i++]){if(elem===results[i]){j=duplicates.push(i)}}while(j--){results.splice(duplicates[j],1)}}sortInput=null;return results};getText=Sizzle.getText=function(elem){var node,ret="",i=0,nodeType=elem.nodeType;if(!nodeType){while(node=elem[i++]){ret+=getText(node)}}else if(nodeType===1||nodeType===9||nodeType===11){if(typeof elem.textContent==="string"){return elem.textContent}else{for(elem=elem.firstChild;elem;elem=elem.nextSibling){ret+=getText(elem)}}}else if(nodeType===3||nodeType===4){return elem.nodeValue}return ret};Expr=Sizzle.selectors={cacheLength:50,createPseudo:markFunction,match:matchExpr,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:true}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:true},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(match){match[1]=match[1].replace(runescape,funescape);match[3]=(match[3]||match[4]||match[5]||"").replace(runescape,funescape);if(match[2]==="~="){match[3]=" "+match[3]+" "}return match.slice(0,4)},CHILD:function(match){match[1]=match[1].toLowerCase();if(match[1].slice(0,3)==="nth"){if(!match[3]){Sizzle.error(match[0])}match[4]=+(match[4]?match[5]+(match[6]||1):2*(match[3]==="even"||match[3]==="odd"));match[5]=+(match[7]+match[8]||match[3]==="odd")}else if(match[3]){Sizzle.error(match[0])}return match},PSEUDO:function(match){var excess,unquoted=!match[6]&&match[2];if(matchExpr["CHILD"].test(match[0])){return null}if(match[3]){match[2]=match[4]||match[5]||""}else if(unquoted&&rpseudo.test(unquoted)&&(excess=tokenize(unquoted,true))&&(excess=unquoted.indexOf(")",unquoted.length-excess)-unquoted.length)){match[0]=match[0].slice(0,excess);match[2]=unquoted.slice(0,excess)}return match.slice(0,3)}},filter:{TAG:function(nodeNameSelector){var nodeName=nodeNameSelector.replace(runescape,funescape).toLowerCase();return nodeNameSelector==="*"?function(){return true}:function(elem){return elem.nodeName&&elem.nodeName.toLowerCase()===nodeName}},CLASS:function(className){var pattern=classCache[className+" "];return pattern||(pattern=new RegExp("(^|"+whitespace+")"+className+"("+whitespace+"|$)"))&&classCache(className,function(elem){return pattern.test(typeof elem.className==="string"&&elem.className||typeof elem.getAttribute!=="undefined"&&elem.getAttribute("class")||"")})},ATTR:function(name,operator,check){return function(elem){var result=Sizzle.attr(elem,name);if(result==null){return operator==="!="}if(!operator){return true}result+="";return operator==="="?result===check:operator==="!="?result!==check:operator==="^="?check&&result.indexOf(check)===0:operator==="*="?check&&result.indexOf(check)>-1:operator==="$="?check&&result.slice(-check.length)===check:operator==="~="?(" "+result.replace(rwhitespace," ")+" ").indexOf(check)>-1:operator==="|="?result===check||result.slice(0,check.length+1)===check+"-":false}},CHILD:function(type,what,argument,first,last){var simple=type.slice(0,3)!=="nth",forward=type.slice(-4)!=="last",ofType=what==="of-type";return first===1&&last===0?function(elem){return!!elem.parentNode}:function(elem,context,xml){var cache,outerCache,node,diff,nodeIndex,start,dir=simple!==forward?"nextSibling":"previousSibling",parent=elem.parentNode,name=ofType&&elem.nodeName.toLowerCase(),useCache=!xml&&!ofType;if(parent){if(simple){while(dir){node=elem;while(node=node[dir]){if(ofType?node.nodeName.toLowerCase()===name:node.nodeType===1){return false}}start=dir=type==="only"&&!start&&"nextSibling"}return true}start=[forward?parent.firstChild:parent.lastChild];if(forward&&useCache){outerCache=parent[expando]||(parent[expando]={});cache=outerCache[type]||[];nodeIndex=cache[0]===dirruns&&cache[1];diff=cache[0]===dirruns&&cache[2];node=nodeIndex&&parent.childNodes[nodeIndex];while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if(node.nodeType===1&&++diff&&node===elem){outerCache[type]=[dirruns,nodeIndex,diff];break}}}else if(useCache&&(cache=(elem[expando]||(elem[expando]={}))[type])&&cache[0]===dirruns){diff=cache[1]}else{while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if((ofType?node.nodeName.toLowerCase()===name:node.nodeType===1)&&++diff){if(useCache){(node[expando]||(node[expando]={}))[type]=[dirruns,diff]}if(node===elem){break}}}}diff-=last;return diff===first||diff%first===0&&diff/first>=0}}},PSEUDO:function(pseudo,argument){var args,fn=Expr.pseudos[pseudo]||Expr.setFilters[pseudo.toLowerCase()]||Sizzle.error("unsupported pseudo: "+pseudo);if(fn[expando]){return fn(argument)}if(fn.length>1){args=[pseudo,pseudo,"",argument];return Expr.setFilters.hasOwnProperty(pseudo.toLowerCase())?markFunction(function(seed,matches){var idx,matched=fn(seed,argument),i=matched.length;while(i--){idx=indexOf(seed,matched[i]);seed[idx]=!(matches[idx]=matched[i])}}):function(elem){return fn(elem,0,args)}}return fn}},pseudos:{not:markFunction(function(selector){var input=[],results=[],matcher=compile(selector.replace(rtrim,"$1"));return matcher[expando]?markFunction(function(seed,matches,context,xml){var elem,unmatched=matcher(seed,null,xml,[]),i=seed.length;while(i--){if(elem=unmatched[i]){seed[i]=!(matches[i]=elem)}}}):function(elem,context,xml){input[0]=elem;matcher(input,null,xml,results);input[0]=null;return!results.pop()}}),has:markFunction(function(selector){return function(elem){return Sizzle(selector,elem).length>0}}),contains:markFunction(function(text){text=text.replace(runescape,funescape);return function(elem){return(elem.textContent||elem.innerText||getText(elem)).indexOf(text)>-1}}),lang:markFunction(function(lang){if(!ridentifier.test(lang||"")){Sizzle.error("unsupported lang: "+lang)}lang=lang.replace(runescape,funescape).toLowerCase();return function(elem){var elemLang;do{if(elemLang=documentIsHTML?elem.lang:elem.getAttribute("xml:lang")||elem.getAttribute("lang")){elemLang=elemLang.toLowerCase();return elemLang===lang||elemLang.indexOf(lang+"-")===0}}while((elem=elem.parentNode)&&elem.nodeType===1);return false}}),target:function(elem){var hash=window.location&&window.location.hash;return hash&&hash.slice(1)===elem.id},root:function(elem){return elem===docElem},focus:function(elem){return elem===document.activeElement&&(!document.hasFocus||document.hasFocus())&&!!(elem.type||elem.href||~elem.tabIndex)},enabled:function(elem){return elem.disabled===false},disabled:function(elem){return elem.disabled===true},checked:function(elem){var nodeName=elem.nodeName.toLowerCase();return nodeName==="input"&&!!elem.checked||nodeName==="option"&&!!elem.selected},selected:function(elem){if(elem.parentNode){elem.parentNode.selectedIndex}return elem.selected===true},empty:function(elem){for(elem=elem.firstChild;elem;elem=elem.nextSibling){if(elem.nodeType<6){return false}}return true},parent:function(elem){return!Expr.pseudos["empty"](elem)},header:function(elem){return rheader.test(elem.nodeName)},input:function(elem){return rinputs.test(elem.nodeName)},button:function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type==="button"||name==="button"},text:function(elem){var attr;return elem.nodeName.toLowerCase()==="input"&&elem.type==="text"&&((attr=elem.getAttribute("type"))==null||attr.toLowerCase()==="text")},first:createPositionalPseudo(function(){return[0]}),last:createPositionalPseudo(function(matchIndexes,length){return[length-1]}),eq:createPositionalPseudo(function(matchIndexes,length,argument){return[argument<0?argument+length:argument]}),even:createPositionalPseudo(function(matchIndexes,length){var i=0;for(;i=0;){matchIndexes.push(i)}return matchIndexes}),gt:createPositionalPseudo(function(matchIndexes,length,argument){var i=argument<0?argument+length:argument;for(;++i1?function(elem,context,xml){var i=matchers.length;while(i--){if(!matchers[i](elem,context,xml)){return false}}return true}:matchers[0]}function multipleContexts(selector,contexts,results){var i=0,len=contexts.length;for(;i-1){seed[temp]=!(results[temp]=elem)}}}}else{matcherOut=condense(matcherOut===results?matcherOut.splice(preexisting,matcherOut.length):matcherOut);if(postFinder){postFinder(null,results,matcherOut,xml)}else{push.apply(results,matcherOut)}}})}function matcherFromTokens(tokens){var checkContext,matcher,j,len=tokens.length,leadingRelative=Expr.relative[tokens[0].type],implicitRelative=leadingRelative||Expr.relative[" "],i=leadingRelative?1:0,matchContext=addCombinator(function(elem){return elem===checkContext},implicitRelative,true),matchAnyContext=addCombinator(function(elem){return indexOf(checkContext,elem)>-1},implicitRelative,true),matchers=[function(elem,context,xml){var ret=!leadingRelative&&(xml||context!==outermostContext)||((checkContext=context).nodeType?matchContext(elem,context,xml):matchAnyContext(elem,context,xml));checkContext=null;return ret}];for(;i1&&elementMatcher(matchers),i>1&&toSelector(tokens.slice(0,i-1).concat({value:tokens[i-2].type===" "?"*":""})).replace(rtrim,"$1"),matcher,i0,byElement=elementMatchers.length>0,superMatcher=function(seed,context,xml,results,outermost){var elem,j,matcher,matchedCount=0,i="0",unmatched=seed&&[],setMatched=[],contextBackup=outermostContext,elems=seed||byElement&&Expr.find["TAG"]("*",outermost),dirrunsUnique=dirruns+=contextBackup==null?1:Math.random()||.1,len=elems.length;if(outermost){outermostContext=context!==document&&context}for(;i!==len&&(elem=elems[i])!=null;i++){if(byElement&&elem){j=0;while(matcher=elementMatchers[j++]){if(matcher(elem,context,xml)){results.push(elem);break}}if(outermost){dirruns=dirrunsUnique}}if(bySet){if(elem=!matcher&&elem){matchedCount--}if(seed){unmatched.push(elem)}}}matchedCount+=i;if(bySet&&i!==matchedCount){j=0;while(matcher=setMatchers[j++]){matcher(unmatched,setMatched,context,xml)}if(seed){if(matchedCount>0){while(i--){if(!(unmatched[i]||setMatched[i])){setMatched[i]=pop.call(results)}}}setMatched=condense(setMatched)}push.apply(results,setMatched);if(outermost&&!seed&&setMatched.length>0&&matchedCount+setMatchers.length>1){Sizzle.uniqueSort(results)}}if(outermost){dirruns=dirrunsUnique;outermostContext=contextBackup}return unmatched};return bySet?markFunction(superMatcher):superMatcher}compile=Sizzle.compile=function(selector,match){var i,setMatchers=[],elementMatchers=[],cached=compilerCache[selector+" "];if(!cached){if(!match){match=tokenize(selector)}i=match.length;while(i--){cached=matcherFromTokens(match[i]);if(cached[expando]){setMatchers.push(cached)}else{elementMatchers.push(cached)}}cached=compilerCache(selector,matcherFromGroupMatchers(elementMatchers,setMatchers));cached.selector=selector}return cached};select=Sizzle.select=function(selector,context,results,seed){var i,tokens,token,type,find,compiled=typeof selector==="function"&&selector,match=!seed&&tokenize(selector=compiled.selector||selector);results=results||[];if(match.length===1){tokens=match[0]=match[0].slice(0);if(tokens.length>2&&(token=tokens[0]).type==="ID"&&support.getById&&context.nodeType===9&&documentIsHTML&&Expr.relative[tokens[1].type]){context=(Expr.find["ID"](token.matches[0].replace(runescape,funescape),context)||[])[0];if(!context){return results}else if(compiled){context=context.parentNode}selector=selector.slice(tokens.shift().value.length)}i=matchExpr["needsContext"].test(selector)?0:tokens.length;while(i--){token=tokens[i];if(Expr.relative[type=token.type]){break}if(find=Expr.find[type]){if(seed=find(token.matches[0].replace(runescape,funescape),rsibling.test(tokens[0].type)&&testContext(context.parentNode)||context)){tokens.splice(i,1);selector=seed.length&&toSelector(tokens);if(!selector){push.apply(results,seed);return results}break}}}}(compiled||compile(selector,match))(seed,context,!documentIsHTML,results,rsibling.test(selector)&&testContext(context.parentNode)||context);return results};support.sortStable=expando.split("").sort(sortOrder).join("")===expando;support.detectDuplicates=!!hasDuplicate;setDocument();support.sortDetached=assert(function(div1){return div1.compareDocumentPosition(document.createElement("div"))&1});if(!assert(function(div){div.innerHTML="";return div.firstChild.getAttribute("href")==="#"})){addHandle("type|href|height|width",function(elem,name,isXML){if(!isXML){return elem.getAttribute(name,name.toLowerCase()==="type"?1:2)}})}if(!support.attributes||!assert(function(div){div.innerHTML="";div.firstChild.setAttribute("value","");return div.firstChild.getAttribute("value")===""})){addHandle("value",function(elem,name,isXML){if(!isXML&&elem.nodeName.toLowerCase()==="input"){return elem.defaultValue}})}if(!assert(function(div){return div.getAttribute("disabled")==null})){addHandle(booleans,function(elem,name,isXML){var val;if(!isXML){return elem[name]===true?name.toLowerCase():(val=elem.getAttributeNode(name))&&val.specified?val.value:null}})}return Sizzle}(window);jQuery.find=Sizzle;jQuery.expr=Sizzle.selectors;jQuery.expr[":"]=jQuery.expr.pseudos;jQuery.unique=Sizzle.uniqueSort;jQuery.text=Sizzle.getText;jQuery.isXMLDoc=Sizzle.isXML;jQuery.contains=Sizzle.contains;var rneedsContext=jQuery.expr.match.needsContext;var rsingleTag=/^<(\w+)\s*\/?>(?:<\/\1>|)$/;var risSimple=/^.[^:#\[\.,]*$/;function winnow(elements,qualifier,not){if(jQuery.isFunction(qualifier)){return jQuery.grep(elements,function(elem,i){return!!qualifier.call(elem,i,elem)!==not})}if(qualifier.nodeType){return jQuery.grep(elements,function(elem){return elem===qualifier!==not})}if(typeof qualifier==="string"){if(risSimple.test(qualifier)){return jQuery.filter(qualifier,elements,not)}qualifier=jQuery.filter(qualifier,elements)}return jQuery.grep(elements,function(elem){return jQuery.inArray(elem,qualifier)>=0!==not})}jQuery.filter=function(expr,elems,not){var elem=elems[0];if(not){expr=":not("+expr+")"}return elems.length===1&&elem.nodeType===1?jQuery.find.matchesSelector(elem,expr)?[elem]:[]:jQuery.find.matches(expr,jQuery.grep(elems,function(elem){return elem.nodeType===1}))};jQuery.fn.extend({find:function(selector){var i,ret=[],self=this,len=self.length;if(typeof selector!=="string"){return this.pushStack(jQuery(selector).filter(function(){for(i=0;i1?jQuery.unique(ret):ret);ret.selector=this.selector?this.selector+" "+selector:selector;return ret},filter:function(selector){return this.pushStack(winnow(this,selector||[],false))},not:function(selector){return this.pushStack(winnow(this,selector||[],true))},is:function(selector){return!!winnow(this,typeof selector==="string"&&rneedsContext.test(selector)?jQuery(selector):selector||[],false).length}});var rootjQuery,document=window.document,rquickExpr=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,init=jQuery.fn.init=function(selector,context){var match,elem;if(!selector){return this}if(typeof selector==="string"){if(selector.charAt(0)==="<"&&selector.charAt(selector.length-1)===">"&&selector.length>=3){match=[null,selector,null]}else{match=rquickExpr.exec(selector)}if(match&&(match[1]||!context)){if(match[1]){context=context instanceof jQuery?context[0]:context;jQuery.merge(this,jQuery.parseHTML(match[1],context&&context.nodeType?context.ownerDocument||context:document,true));if(rsingleTag.test(match[1])&&jQuery.isPlainObject(context)){for(match in context){if(jQuery.isFunction(this[match])){this[match](context[match])}else{this.attr(match,context[match])}}}return this}else{elem=document.getElementById(match[2]);if(elem&&elem.parentNode){if(elem.id!==match[2]){return rootjQuery.find(selector)}this.length=1;this[0]=elem}this.context=document;this.selector=selector;return this}}else if(!context||context.jquery){return(context||rootjQuery).find(selector)}else{return this.constructor(context).find(selector)}}else if(selector.nodeType){this.context=this[0]=selector;this.length=1;return this}else if(jQuery.isFunction(selector)){return typeof rootjQuery.ready!=="undefined"?rootjQuery.ready(selector):selector(jQuery)}if(selector.selector!==undefined){this.selector=selector.selector;this.context=selector.context}return jQuery.makeArray(selector,this)};init.prototype=jQuery.fn;rootjQuery=jQuery(document);var rparentsprev=/^(?:parents|prev(?:Until|All))/,guaranteedUnique={children:true,contents:true,next:true,prev:true};jQuery.extend({dir:function(elem,dir,until){var matched=[],cur=elem[dir];while(cur&&cur.nodeType!==9&&(until===undefined||cur.nodeType!==1||!jQuery(cur).is(until))){if(cur.nodeType===1){matched.push(cur)}cur=cur[dir]}return matched},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType===1&&n!==elem){r.push(n)}}return r}});jQuery.fn.extend({has:function(target){var i,targets=jQuery(target,this),len=targets.length;return this.filter(function(){for(i=0;i-1:cur.nodeType===1&&jQuery.find.matchesSelector(cur,selectors))){matched.push(cur);break}}}return this.pushStack(matched.length>1?jQuery.unique(matched):matched)},index:function(elem){if(!elem){return this[0]&&this[0].parentNode?this.first().prevAll().length:-1}if(typeof elem==="string"){return jQuery.inArray(this[0],jQuery(elem))}return jQuery.inArray(elem.jquery?elem[0]:elem,this)},add:function(selector,context){return this.pushStack(jQuery.unique(jQuery.merge(this.get(),jQuery(selector,context))))},addBack:function(selector){return this.add(selector==null?this.prevObject:this.prevObject.filter(selector))}});function sibling(cur,dir){do{cur=cur[dir]}while(cur&&cur.nodeType!==1);return cur}jQuery.each({parent:function(elem){var parent=elem.parentNode;return parent&&parent.nodeType!==11?parent:null},parents:function(elem){return jQuery.dir(elem,"parentNode")},parentsUntil:function(elem,i,until){return jQuery.dir(elem,"parentNode",until)},next:function(elem){return sibling(elem,"nextSibling")},prev:function(elem){return sibling(elem,"previousSibling")},nextAll:function(elem){return jQuery.dir(elem,"nextSibling")},prevAll:function(elem){return jQuery.dir(elem,"previousSibling")},nextUntil:function(elem,i,until){return jQuery.dir(elem,"nextSibling",until)},prevUntil:function(elem,i,until){return jQuery.dir(elem,"previousSibling",until)},siblings:function(elem){return jQuery.sibling((elem.parentNode||{}).firstChild,elem)},children:function(elem){return jQuery.sibling(elem.firstChild)},contents:function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.merge([],elem.childNodes)}},function(name,fn){jQuery.fn[name]=function(until,selector){var ret=jQuery.map(this,fn,until);if(name.slice(-5)!=="Until"){selector=until}if(selector&&typeof selector==="string"){ret=jQuery.filter(selector,ret)}if(this.length>1){if(!guaranteedUnique[name]){ret=jQuery.unique(ret)}if(rparentsprev.test(name)){ret=ret.reverse()}}return this.pushStack(ret)}});var rnotwhite=/\S+/g;var optionsCache={};function createOptions(options){var object=optionsCache[options]={};jQuery.each(options.match(rnotwhite)||[],function(_,flag){object[flag]=true});return object}jQuery.Callbacks=function(options){options=typeof options==="string"?optionsCache[options]||createOptions(options):jQuery.extend({},options);var firing,memory,fired,firingLength,firingIndex,firingStart,list=[],stack=!options.once&&[],fire=function(data){memory=options.memory&&data;fired=true;firingIndex=firingStart||0;firingStart=0;firingLength=list.length;firing=true;for(;list&&firingIndex-1){list.splice(index,1);if(firing){if(index<=firingLength){firingLength--}if(index<=firingIndex){firingIndex--}}}})}return this},has:function(fn){return fn?jQuery.inArray(fn,list)>-1:!!(list&&list.length)},empty:function(){list=[];firingLength=0;return this},disable:function(){list=stack=memory=undefined;return this},disabled:function(){return!list},lock:function(){stack=undefined;if(!memory){self.disable()}return this},locked:function(){return!stack},fireWith:function(context,args){if(list&&(!fired||stack)){args=args||[];args=[context,args.slice?args.slice():args];if(firing){stack.push(args)}else{fire(args)}}return this},fire:function(){self.fireWith(this,arguments);return this},fired:function(){return!!fired}};return self};jQuery.extend({Deferred:function(func){var tuples=[["resolve","done",jQuery.Callbacks("once memory"),"resolved"],["reject","fail",jQuery.Callbacks("once memory"),"rejected"],["notify","progress",jQuery.Callbacks("memory")]],state="pending",promise={state:function(){return state},always:function(){deferred.done(arguments).fail(arguments);return this},then:function(){var fns=arguments;return jQuery.Deferred(function(newDefer){jQuery.each(tuples,function(i,tuple){var fn=jQuery.isFunction(fns[i])&&fns[i];deferred[tuple[1]](function(){var returned=fn&&fn.apply(this,arguments);if(returned&&jQuery.isFunction(returned.promise)){returned.promise().done(newDefer.resolve).fail(newDefer.reject).progress(newDefer.notify)}else{newDefer[tuple[0]+"With"](this===promise?newDefer.promise():this,fn?[returned]:arguments)}})});fns=null}).promise()},promise:function(obj){return obj!=null?jQuery.extend(obj,promise):promise}},deferred={};promise.pipe=promise.then;jQuery.each(tuples,function(i,tuple){var list=tuple[2],stateString=tuple[3];promise[tuple[1]]=list.add;if(stateString){list.add(function(){state=stateString},tuples[i^1][2].disable,tuples[2][2].lock)}deferred[tuple[0]]=function(){deferred[tuple[0]+"With"](this===deferred?promise:this,arguments);return this};deferred[tuple[0]+"With"]=list.fireWith});promise.promise(deferred);if(func){func.call(deferred,deferred)}return deferred},when:function(subordinate){var i=0,resolveValues=slice.call(arguments),length=resolveValues.length,remaining=length!==1||subordinate&&jQuery.isFunction(subordinate.promise)?length:0,deferred=remaining===1?subordinate:jQuery.Deferred(),updateFunc=function(i,contexts,values){return function(value){contexts[i]=this;values[i]=arguments.length>1?slice.call(arguments):value;if(values===progressValues){deferred.notifyWith(contexts,values)}else if(!--remaining){deferred.resolveWith(contexts,values)}}},progressValues,progressContexts,resolveContexts;if(length>1){progressValues=new Array(length);progressContexts=new Array(length);resolveContexts=new Array(length);for(;i0){return}readyList.resolveWith(document,[jQuery]);if(jQuery.fn.triggerHandler){jQuery(document).triggerHandler("ready");jQuery(document).off("ready")}}});function detach(){if(document.addEventListener){document.removeEventListener("DOMContentLoaded",completed,false);window.removeEventListener("load",completed,false)}else{document.detachEvent("onreadystatechange",completed);window.detachEvent("onload",completed)}}function completed(){if(document.addEventListener||event.type==="load"||document.readyState==="complete"){detach();jQuery.ready()}}jQuery.ready.promise=function(obj){if(!readyList){readyList=jQuery.Deferred();if(document.readyState==="complete"){setTimeout(jQuery.ready)}else if(document.addEventListener){document.addEventListener("DOMContentLoaded",completed,false);window.addEventListener("load",completed,false)}else{document.attachEvent("onreadystatechange",completed);window.attachEvent("onload",completed);var top=false;try{top=window.frameElement==null&&document.documentElement}catch(e){}if(top&&top.doScroll){(function doScrollCheck(){if(!jQuery.isReady){try{top.doScroll("left")}catch(e){return setTimeout(doScrollCheck,50)}detach();jQuery.ready()}})()}}}return readyList.promise(obj)};var strundefined=typeof undefined;var i;for(i in jQuery(support)){break}support.ownLast=i!=="0";support.inlineBlockNeedsLayout=false;jQuery(function(){var val,div,body,container;body=document.getElementsByTagName("body")[0];if(!body||!body.style){return}div=document.createElement("div");container=document.createElement("div");container.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px";body.appendChild(container).appendChild(div);if(typeof div.style.zoom!==strundefined){div.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1";support.inlineBlockNeedsLayout=val=div.offsetWidth===3;if(val){body.style.zoom=1}}body.removeChild(container)});(function(){var div=document.createElement("div");if(support.deleteExpando==null){support.deleteExpando=true;try{delete div.test}catch(e){support.deleteExpando=false}}div=null})();jQuery.acceptData=function(elem){var noData=jQuery.noData[(elem.nodeName+" ").toLowerCase()],nodeType=+elem.nodeType||1;return nodeType!==1&&nodeType!==9?false:!noData||noData!==true&&elem.getAttribute("classid")===noData};var rbrace=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,rmultiDash=/([A-Z])/g;function dataAttr(elem,key,data){if(data===undefined&&elem.nodeType===1){var name="data-"+key.replace(rmultiDash,"-$1").toLowerCase();data=elem.getAttribute(name);if(typeof data==="string"){try{data=data==="true"?true:data==="false"?false:data==="null"?null:+data+""===data?+data:rbrace.test(data)?jQuery.parseJSON(data):data}catch(e){}jQuery.data(elem,key,data)}else{data=undefined}}return data}function isEmptyDataObject(obj){var name;for(name in obj){if(name==="data"&&jQuery.isEmptyObject(obj[name])){continue}if(name!=="toJSON"){return false}}return true}function internalData(elem,name,data,pvt){if(!jQuery.acceptData(elem)){return}var ret,thisCache,internalKey=jQuery.expando,isNode=elem.nodeType,cache=isNode?jQuery.cache:elem,id=isNode?elem[internalKey]:elem[internalKey]&&internalKey;if((!id||!cache[id]||!pvt&&!cache[id].data)&&data===undefined&&typeof name==="string"){return}if(!id){if(isNode){id=elem[internalKey]=deletedIds.pop()||jQuery.guid++}else{id=internalKey}}if(!cache[id]){cache[id]=isNode?{}:{toJSON:jQuery.noop}}if(typeof name==="object"||typeof name==="function"){if(pvt){cache[id]=jQuery.extend(cache[id],name)}else{cache[id].data=jQuery.extend(cache[id].data,name)}}thisCache=cache[id];if(!pvt){if(!thisCache.data){thisCache.data={}}thisCache=thisCache.data}if(data!==undefined){thisCache[jQuery.camelCase(name)]=data}if(typeof name==="string"){ret=thisCache[name];if(ret==null){ret=thisCache[jQuery.camelCase(name)]}}else{ret=thisCache}return ret}function internalRemoveData(elem,name,pvt){if(!jQuery.acceptData(elem)){return}var thisCache,i,isNode=elem.nodeType,cache=isNode?jQuery.cache:elem,id=isNode?elem[jQuery.expando]:jQuery.expando;if(!cache[id]){return}if(name){thisCache=pvt?cache[id]:cache[id].data;if(thisCache){if(!jQuery.isArray(name)){if(name in thisCache){name=[name]}else{name=jQuery.camelCase(name);if(name in thisCache){name=[name]}else{name=name.split(" ")}}}else{name=name.concat(jQuery.map(name,jQuery.camelCase))}i=name.length;while(i--){delete thisCache[name[i]]}if(pvt?!isEmptyDataObject(thisCache):!jQuery.isEmptyObject(thisCache)){return}}}if(!pvt){delete cache[id].data;if(!isEmptyDataObject(cache[id])){return}}if(isNode){jQuery.cleanData([elem],true)}else if(support.deleteExpando||cache!=cache.window){delete cache[id]}else{cache[id]=null}}jQuery.extend({cache:{},noData:{"applet ":true,"embed ":true,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(elem){elem=elem.nodeType?jQuery.cache[elem[jQuery.expando]]:elem[jQuery.expando];return!!elem&&!isEmptyDataObject(elem)},data:function(elem,name,data){return internalData(elem,name,data)},removeData:function(elem,name){return internalRemoveData(elem,name)},_data:function(elem,name,data){return internalData(elem,name,data,true)},_removeData:function(elem,name){return internalRemoveData(elem,name,true)}});jQuery.fn.extend({data:function(key,value){var i,name,data,elem=this[0],attrs=elem&&elem.attributes;if(key===undefined){if(this.length){data=jQuery.data(elem);if(elem.nodeType===1&&!jQuery._data(elem,"parsedAttrs")){i=attrs.length;while(i--){if(attrs[i]){name=attrs[i].name;if(name.indexOf("data-")===0){name=jQuery.camelCase(name.slice(5));dataAttr(elem,name,data[name])}}}jQuery._data(elem,"parsedAttrs",true)}}return data}if(typeof key==="object"){return this.each(function(){jQuery.data(this,key)})}return arguments.length>1?this.each(function(){jQuery.data(this,key,value)}):elem?dataAttr(elem,key,jQuery.data(elem,key)):undefined},removeData:function(key){return this.each(function(){jQuery.removeData(this,key)})}});jQuery.extend({queue:function(elem,type,data){var queue;if(elem){type=(type||"fx")+"queue";queue=jQuery._data(elem,type);if(data){if(!queue||jQuery.isArray(data)){queue=jQuery._data(elem,type,jQuery.makeArray(data))}else{queue.push(data)}}return queue||[]}},dequeue:function(elem,type){type=type||"fx";var queue=jQuery.queue(elem,type),startLength=queue.length,fn=queue.shift(),hooks=jQuery._queueHooks(elem,type),next=function(){jQuery.dequeue(elem,type)};if(fn==="inprogress"){fn=queue.shift();startLength--}if(fn){if(type==="fx"){queue.unshift("inprogress")}delete hooks.stop;fn.call(elem,next,hooks)}if(!startLength&&hooks){hooks.empty.fire()}},_queueHooks:function(elem,type){var key=type+"queueHooks";return jQuery._data(elem,key)||jQuery._data(elem,key,{empty:jQuery.Callbacks("once memory").add(function(){jQuery._removeData(elem,type+"queue");jQuery._removeData(elem,key)})})}});jQuery.fn.extend({queue:function(type,data){var setter=2;if(typeof type!=="string"){data=type;type="fx";setter--}if(arguments.length
a";support.leadingWhitespace=div.firstChild.nodeType===3;support.tbody=!div.getElementsByTagName("tbody").length;support.htmlSerialize=!!div.getElementsByTagName("link").length;support.html5Clone=document.createElement("nav").cloneNode(true).outerHTML!=="<:nav>";input.type="checkbox";input.checked=true;fragment.appendChild(input);support.appendChecked=input.checked;div.innerHTML="";support.noCloneChecked=!!div.cloneNode(true).lastChild.defaultValue;fragment.appendChild(div);div.innerHTML="";support.checkClone=div.cloneNode(true).cloneNode(true).lastChild.checked;support.noCloneEvent=true;if(div.attachEvent){div.attachEvent("onclick",function(){support.noCloneEvent=false});div.cloneNode(true).click()}if(support.deleteExpando==null){support.deleteExpando=true;try{delete div.test}catch(e){support.deleteExpando=false}}})();(function(){var i,eventName,div=document.createElement("div");for(i in{submit:true,change:true,focusin:true}){eventName="on"+i;if(!(support[i+"Bubbles"]=eventName in window)){div.setAttribute(eventName,"t");support[i+"Bubbles"]=div.attributes[eventName].expando===false}}div=null})();var rformElems=/^(?:input|select|textarea)$/i,rkeyEvent=/^key/,rmouseEvent=/^(?:mouse|pointer|contextmenu)|click/,rfocusMorph=/^(?:focusinfocus|focusoutblur)$/,rtypenamespace=/^([^.]*)(?:\.(.+)|)$/;function returnTrue(){return true}function returnFalse(){return false}function safeActiveElement(){try{return document.activeElement}catch(err){}}jQuery.event={global:{},add:function(elem,types,handler,data,selector){var tmp,events,t,handleObjIn,special,eventHandle,handleObj,handlers,type,namespaces,origType,elemData=jQuery._data(elem);if(!elemData){return}if(handler.handler){handleObjIn=handler;handler=handleObjIn.handler;selector=handleObjIn.selector}if(!handler.guid){handler.guid=jQuery.guid++}if(!(events=elemData.events)){events=elemData.events={}}if(!(eventHandle=elemData.handle)){eventHandle=elemData.handle=function(e){return typeof jQuery!==strundefined&&(!e||jQuery.event.triggered!==e.type)?jQuery.event.dispatch.apply(eventHandle.elem,arguments):undefined};eventHandle.elem=elem}types=(types||"").match(rnotwhite)||[""];t=types.length;while(t--){tmp=rtypenamespace.exec(types[t])||[];type=origType=tmp[1];namespaces=(tmp[2]||"").split(".").sort();if(!type){continue}special=jQuery.event.special[type]||{};type=(selector?special.delegateType:special.bindType)||type;special=jQuery.event.special[type]||{};handleObj=jQuery.extend({type:type,origType:origType,data:data,handler:handler,guid:handler.guid,selector:selector,needsContext:selector&&jQuery.expr.match.needsContext.test(selector),namespace:namespaces.join(".")},handleObjIn);if(!(handlers=events[type])){handlers=events[type]=[];handlers.delegateCount=0;if(!special.setup||special.setup.call(elem,data,namespaces,eventHandle)===false){if(elem.addEventListener){elem.addEventListener(type,eventHandle,false)}else if(elem.attachEvent){elem.attachEvent("on"+type,eventHandle)}}}if(special.add){special.add.call(elem,handleObj);if(!handleObj.handler.guid){handleObj.handler.guid=handler.guid}}if(selector){handlers.splice(handlers.delegateCount++,0,handleObj)}else{handlers.push(handleObj)}jQuery.event.global[type]=true}elem=null},remove:function(elem,types,handler,selector,mappedTypes){var j,handleObj,tmp,origCount,t,events,special,handlers,type,namespaces,origType,elemData=jQuery.hasData(elem)&&jQuery._data(elem);if(!elemData||!(events=elemData.events)){return}types=(types||"").match(rnotwhite)||[""];t=types.length;while(t--){tmp=rtypenamespace.exec(types[t])||[];type=origType=tmp[1];namespaces=(tmp[2]||"").split(".").sort();if(!type){for(type in events){jQuery.event.remove(elem,type+types[t],handler,selector,true)}continue}special=jQuery.event.special[type]||{};type=(selector?special.delegateType:special.bindType)||type;handlers=events[type]||[];tmp=tmp[2]&&new RegExp("(^|\\.)"+namespaces.join("\\.(?:.*\\.|)")+"(\\.|$)");origCount=j=handlers.length;while(j--){handleObj=handlers[j];if((mappedTypes||origType===handleObj.origType)&&(!handler||handler.guid===handleObj.guid)&&(!tmp||tmp.test(handleObj.namespace))&&(!selector||selector===handleObj.selector||selector==="**"&&handleObj.selector)){handlers.splice(j,1);if(handleObj.selector){handlers.delegateCount--}if(special.remove){special.remove.call(elem,handleObj)}}}if(origCount&&!handlers.length){if(!special.teardown||special.teardown.call(elem,namespaces,elemData.handle)===false){jQuery.removeEvent(elem,type,elemData.handle)}delete events[type]}}if(jQuery.isEmptyObject(events)){delete elemData.handle;jQuery._removeData(elem,"events")}},trigger:function(event,data,elem,onlyHandlers){var handle,ontype,cur,bubbleType,special,tmp,i,eventPath=[elem||document],type=hasOwn.call(event,"type")?event.type:event,namespaces=hasOwn.call(event,"namespace")?event.namespace.split("."):[];cur=tmp=elem=elem||document;if(elem.nodeType===3||elem.nodeType===8){return}if(rfocusMorph.test(type+jQuery.event.triggered)){return}if(type.indexOf(".")>=0){namespaces=type.split(".");type=namespaces.shift();namespaces.sort()}ontype=type.indexOf(":")<0&&"on"+type;event=event[jQuery.expando]?event:new jQuery.Event(type,typeof event==="object"&&event);event.isTrigger=onlyHandlers?2:3;event.namespace=namespaces.join(".");event.namespace_re=event.namespace?new RegExp("(^|\\.)"+namespaces.join("\\.(?:.*\\.|)")+"(\\.|$)"):null;event.result=undefined;if(!event.target){event.target=elem}data=data==null?[event]:jQuery.makeArray(data,[event]);special=jQuery.event.special[type]||{};if(!onlyHandlers&&special.trigger&&special.trigger.apply(elem,data)===false){return}if(!onlyHandlers&&!special.noBubble&&!jQuery.isWindow(elem)){bubbleType=special.delegateType||type;if(!rfocusMorph.test(bubbleType+type)){cur=cur.parentNode}for(;cur;cur=cur.parentNode){eventPath.push(cur);tmp=cur}if(tmp===(elem.ownerDocument||document)){eventPath.push(tmp.defaultView||tmp.parentWindow||window)}}i=0;while((cur=eventPath[i++])&&!event.isPropagationStopped()){event.type=i>1?bubbleType:special.bindType||type;handle=(jQuery._data(cur,"events")||{})[event.type]&&jQuery._data(cur,"handle");if(handle){handle.apply(cur,data)}handle=ontype&&cur[ontype];if(handle&&handle.apply&&jQuery.acceptData(cur)){event.result=handle.apply(cur,data);if(event.result===false){event.preventDefault()}}}event.type=type;if(!onlyHandlers&&!event.isDefaultPrevented()){if((!special._default||special._default.apply(eventPath.pop(),data)===false)&&jQuery.acceptData(elem)){if(ontype&&elem[type]&&!jQuery.isWindow(elem)){tmp=elem[ontype];if(tmp){elem[ontype]=null}jQuery.event.triggered=type;try{elem[type]()}catch(e){}jQuery.event.triggered=undefined;if(tmp){elem[ontype]=tmp}}}}return event.result},dispatch:function(event){event=jQuery.event.fix(event);var i,ret,handleObj,matched,j,handlerQueue=[],args=slice.call(arguments),handlers=(jQuery._data(this,"events")||{})[event.type]||[],special=jQuery.event.special[event.type]||{};args[0]=event;event.delegateTarget=this;if(special.preDispatch&&special.preDispatch.call(this,event)===false){return}handlerQueue=jQuery.event.handlers.call(this,event,handlers);i=0;while((matched=handlerQueue[i++])&&!event.isPropagationStopped()){event.currentTarget=matched.elem;j=0;while((handleObj=matched.handlers[j++])&&!event.isImmediatePropagationStopped()){if(!event.namespace_re||event.namespace_re.test(handleObj.namespace)){event.handleObj=handleObj;event.data=handleObj.data;ret=((jQuery.event.special[handleObj.origType]||{}).handle||handleObj.handler).apply(matched.elem,args); -if(ret!==undefined){if((event.result=ret)===false){event.preventDefault();event.stopPropagation()}}}}}if(special.postDispatch){special.postDispatch.call(this,event)}return event.result},handlers:function(event,handlers){var sel,handleObj,matches,i,handlerQueue=[],delegateCount=handlers.delegateCount,cur=event.target;if(delegateCount&&cur.nodeType&&(!event.button||event.type!=="click")){for(;cur!=this;cur=cur.parentNode||this){if(cur.nodeType===1&&(cur.disabled!==true||event.type!=="click")){matches=[];for(i=0;i=0:jQuery.find(sel,this,null,[cur]).length}if(matches[sel]){matches.push(handleObj)}}if(matches.length){handlerQueue.push({elem:cur,handlers:matches})}}}}if(delegateCount]","i"),rleadingWhitespace=/^\s+/,rxhtmlTag=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,rtagName=/<([\w:]+)/,rtbody=/\s*$/g,wrapMap={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:support.htmlSerialize?[0,"",""]:[1,"X
","
"]},safeFragment=createSafeFragment(document),fragmentDiv=safeFragment.appendChild(document.createElement("div"));wrapMap.optgroup=wrapMap.option;wrapMap.tbody=wrapMap.tfoot=wrapMap.colgroup=wrapMap.caption=wrapMap.thead;wrapMap.th=wrapMap.td;function getAll(context,tag){var elems,elem,i=0,found=typeof context.getElementsByTagName!==strundefined?context.getElementsByTagName(tag||"*"):typeof context.querySelectorAll!==strundefined?context.querySelectorAll(tag||"*"):undefined;if(!found){for(found=[],elems=context.childNodes||context;(elem=elems[i])!=null;i++){if(!tag||jQuery.nodeName(elem,tag)){found.push(elem)}else{jQuery.merge(found,getAll(elem,tag))}}}return tag===undefined||tag&&jQuery.nodeName(context,tag)?jQuery.merge([context],found):found}function fixDefaultChecked(elem){if(rcheckableType.test(elem.type)){elem.defaultChecked=elem.checked}}function manipulationTarget(elem,content){return jQuery.nodeName(elem,"table")&&jQuery.nodeName(content.nodeType!==11?content:content.firstChild,"tr")?elem.getElementsByTagName("tbody")[0]||elem.appendChild(elem.ownerDocument.createElement("tbody")):elem}function disableScript(elem){elem.type=(jQuery.find.attr(elem,"type")!==null)+"/"+elem.type;return elem}function restoreScript(elem){var match=rscriptTypeMasked.exec(elem.type);if(match){elem.type=match[1]}else{elem.removeAttribute("type")}return elem}function setGlobalEval(elems,refElements){var elem,i=0;for(;(elem=elems[i])!=null;i++){jQuery._data(elem,"globalEval",!refElements||jQuery._data(refElements[i],"globalEval"))}}function cloneCopyEvent(src,dest){if(dest.nodeType!==1||!jQuery.hasData(src)){return}var type,i,l,oldData=jQuery._data(src),curData=jQuery._data(dest,oldData),events=oldData.events;if(events){delete curData.handle;curData.events={};for(type in events){for(i=0,l=events[type].length;i")){clone=elem.cloneNode(true)}else{fragmentDiv.innerHTML=elem.outerHTML;fragmentDiv.removeChild(clone=fragmentDiv.firstChild)}if((!support.noCloneEvent||!support.noCloneChecked)&&(elem.nodeType===1||elem.nodeType===11)&&!jQuery.isXMLDoc(elem)){destElements=getAll(clone);srcElements=getAll(elem);for(i=0;(node=srcElements[i])!=null;++i){if(destElements[i]){fixCloneNodeIssues(node,destElements[i])}}}if(dataAndEvents){if(deepDataAndEvents){srcElements=srcElements||getAll(elem);destElements=destElements||getAll(clone);for(i=0;(node=srcElements[i])!=null;i++){cloneCopyEvent(node,destElements[i])}}else{cloneCopyEvent(elem,clone)}}destElements=getAll(clone,"script");if(destElements.length>0){setGlobalEval(destElements,!inPage&&getAll(elem,"script"))}destElements=srcElements=node=null;return clone},buildFragment:function(elems,context,scripts,selection){var j,elem,contains,tmp,tag,tbody,wrap,l=elems.length,safe=createSafeFragment(context),nodes=[],i=0;for(;i")+wrap[2];j=wrap[0];while(j--){tmp=tmp.lastChild}if(!support.leadingWhitespace&&rleadingWhitespace.test(elem)){nodes.push(context.createTextNode(rleadingWhitespace.exec(elem)[0]))}if(!support.tbody){elem=tag==="table"&&!rtbody.test(elem)?tmp.firstChild:wrap[1]===""&&!rtbody.test(elem)?tmp:0;j=elem&&elem.childNodes.length;while(j--){if(jQuery.nodeName(tbody=elem.childNodes[j],"tbody")&&!tbody.childNodes.length){elem.removeChild(tbody)}}}jQuery.merge(nodes,tmp.childNodes);tmp.textContent="";while(tmp.firstChild){tmp.removeChild(tmp.firstChild)}tmp=safe.lastChild}}}if(tmp){safe.removeChild(tmp)}if(!support.appendChecked){jQuery.grep(getAll(nodes,"input"),fixDefaultChecked)}i=0;while(elem=nodes[i++]){if(selection&&jQuery.inArray(elem,selection)!==-1){continue}contains=jQuery.contains(elem.ownerDocument,elem);tmp=getAll(safe.appendChild(elem),"script");if(contains){setGlobalEval(tmp)}if(scripts){j=0;while(elem=tmp[j++]){if(rscriptType.test(elem.type||"")){scripts.push(elem)}}}}tmp=null;return safe},cleanData:function(elems,acceptData){var elem,type,id,data,i=0,internalKey=jQuery.expando,cache=jQuery.cache,deleteExpando=support.deleteExpando,special=jQuery.event.special;for(;(elem=elems[i])!=null;i++){if(acceptData||jQuery.acceptData(elem)){id=elem[internalKey];data=id&&cache[id];if(data){if(data.events){for(type in data.events){if(special[type]){jQuery.event.remove(elem,type)}else{jQuery.removeEvent(elem,type,data.handle)}}}if(cache[id]){delete cache[id];if(deleteExpando){delete elem[internalKey]}else if(typeof elem.removeAttribute!==strundefined){elem.removeAttribute(internalKey)}else{elem[internalKey]=null}deletedIds.push(id)}}}}}});jQuery.fn.extend({text:function(value){return access(this,function(value){return value===undefined?jQuery.text(this):this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(value))},null,value,arguments.length)},append:function(){return this.domManip(arguments,function(elem){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var target=manipulationTarget(this,elem);target.appendChild(elem)}})},prepend:function(){return this.domManip(arguments,function(elem){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var target=manipulationTarget(this,elem);target.insertBefore(elem,target.firstChild)}})},before:function(){return this.domManip(arguments,function(elem){if(this.parentNode){this.parentNode.insertBefore(elem,this)}})},after:function(){return this.domManip(arguments,function(elem){if(this.parentNode){this.parentNode.insertBefore(elem,this.nextSibling)}})},remove:function(selector,keepData){var elem,elems=selector?jQuery.filter(selector,this):this,i=0;for(;(elem=elems[i])!=null;i++){if(!keepData&&elem.nodeType===1){jQuery.cleanData(getAll(elem))}if(elem.parentNode){if(keepData&&jQuery.contains(elem.ownerDocument,elem)){setGlobalEval(getAll(elem,"script"))}elem.parentNode.removeChild(elem)}}return this},empty:function(){var elem,i=0;for(;(elem=this[i])!=null;i++){if(elem.nodeType===1){jQuery.cleanData(getAll(elem,false))}while(elem.firstChild){elem.removeChild(elem.firstChild)}if(elem.options&&jQuery.nodeName(elem,"select")){elem.options.length=0}}return this},clone:function(dataAndEvents,deepDataAndEvents){dataAndEvents=dataAndEvents==null?false:dataAndEvents;deepDataAndEvents=deepDataAndEvents==null?dataAndEvents:deepDataAndEvents;return this.map(function(){return jQuery.clone(this,dataAndEvents,deepDataAndEvents)})},html:function(value){return access(this,function(value){var elem=this[0]||{},i=0,l=this.length;if(value===undefined){return elem.nodeType===1?elem.innerHTML.replace(rinlinejQuery,""):undefined}if(typeof value==="string"&&!rnoInnerhtml.test(value)&&(support.htmlSerialize||!rnoshimcache.test(value))&&(support.leadingWhitespace||!rleadingWhitespace.test(value))&&!wrapMap[(rtagName.exec(value)||["",""])[1].toLowerCase()]){value=value.replace(rxhtmlTag,"<$1>");try{for(;i1&&typeof value==="string"&&!support.checkClone&&rchecked.test(value)){return this.each(function(index){var self=set.eq(index);if(isFunction){args[0]=value.call(this,index,self.html())}self.domManip(args,callback)})}if(l){fragment=jQuery.buildFragment(args,this[0].ownerDocument,false,this);first=fragment.firstChild;if(fragment.childNodes.length===1){fragment=first}if(first){scripts=jQuery.map(getAll(fragment,"script"),disableScript);hasScripts=scripts.length;for(;i")).appendTo(doc.documentElement);doc=(iframe[0].contentWindow||iframe[0].contentDocument).document;doc.write();doc.close();display=actualDisplay(nodeName,doc);iframe.detach()}elemdisplay[nodeName]=display}return display}(function(){var shrinkWrapBlocksVal;support.shrinkWrapBlocks=function(){if(shrinkWrapBlocksVal!=null){return shrinkWrapBlocksVal}shrinkWrapBlocksVal=false;var div,body,container;body=document.getElementsByTagName("body")[0];if(!body||!body.style){return}div=document.createElement("div");container=document.createElement("div");container.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px";body.appendChild(container).appendChild(div);if(typeof div.style.zoom!==strundefined){div.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;"+"box-sizing:content-box;display:block;margin:0;border:0;"+"padding:1px;width:1px;zoom:1";div.appendChild(document.createElement("div")).style.width="5px";shrinkWrapBlocksVal=div.offsetWidth!==3}body.removeChild(container);return shrinkWrapBlocksVal}})();var rmargin=/^margin/;var rnumnonpx=new RegExp("^("+pnum+")(?!px)[a-z%]+$","i");var getStyles,curCSS,rposition=/^(top|right|bottom|left)$/;if(window.getComputedStyle){getStyles=function(elem){if(elem.ownerDocument.defaultView.opener){return elem.ownerDocument.defaultView.getComputedStyle(elem,null)}return window.getComputedStyle(elem,null)};curCSS=function(elem,name,computed){var width,minWidth,maxWidth,ret,style=elem.style;computed=computed||getStyles(elem);ret=computed?computed.getPropertyValue(name)||computed[name]:undefined;if(computed){if(ret===""&&!jQuery.contains(elem.ownerDocument,elem)){ret=jQuery.style(elem,name)}if(rnumnonpx.test(ret)&&rmargin.test(name)){width=style.width;minWidth=style.minWidth;maxWidth=style.maxWidth;style.minWidth=style.maxWidth=style.width=ret;ret=computed.width;style.width=width;style.minWidth=minWidth;style.maxWidth=maxWidth}}return ret===undefined?ret:ret+""}}else if(document.documentElement.currentStyle){getStyles=function(elem){return elem.currentStyle};curCSS=function(elem,name,computed){var left,rs,rsLeft,ret,style=elem.style;computed=computed||getStyles(elem);ret=computed?computed[name]:undefined;if(ret==null&&style&&style[name]){ret=style[name]}if(rnumnonpx.test(ret)&&!rposition.test(name)){left=style.left;rs=elem.runtimeStyle;rsLeft=rs&&rs.left;if(rsLeft){rs.left=elem.currentStyle.left}style.left=name==="fontSize"?"1em":ret;ret=style.pixelLeft+"px";style.left=left;if(rsLeft){rs.left=rsLeft}}return ret===undefined?ret:ret+""||"auto"}}function addGetHookIf(conditionFn,hookFn){return{get:function(){var condition=conditionFn();if(condition==null){return}if(condition){delete this.get;return}return(this.get=hookFn).apply(this,arguments)}}}(function(){var div,style,a,pixelPositionVal,boxSizingReliableVal,reliableHiddenOffsetsVal,reliableMarginRightVal;div=document.createElement("div");div.innerHTML="
a";a=div.getElementsByTagName("a")[0];style=a&&a.style;if(!style){return}style.cssText="float:left;opacity:.5";support.opacity=style.opacity==="0.5";support.cssFloat=!!style.cssFloat;div.style.backgroundClip="content-box";div.cloneNode(true).style.backgroundClip="";support.clearCloneStyle=div.style.backgroundClip==="content-box";support.boxSizing=style.boxSizing===""||style.MozBoxSizing===""||style.WebkitBoxSizing==="";jQuery.extend(support,{reliableHiddenOffsets:function(){if(reliableHiddenOffsetsVal==null){computeStyleTests()}return reliableHiddenOffsetsVal},boxSizingReliable:function(){if(boxSizingReliableVal==null){computeStyleTests()}return boxSizingReliableVal},pixelPosition:function(){if(pixelPositionVal==null){computeStyleTests()}return pixelPositionVal},reliableMarginRight:function(){if(reliableMarginRightVal==null){computeStyleTests()}return reliableMarginRightVal}});function computeStyleTests(){var div,body,container,contents;body=document.getElementsByTagName("body")[0];if(!body||!body.style){return}div=document.createElement("div");container=document.createElement("div");container.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px";body.appendChild(container).appendChild(div);div.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;"+"box-sizing:border-box;display:block;margin-top:1%;top:1%;"+"border:1px;padding:1px;width:4px;position:absolute";pixelPositionVal=boxSizingReliableVal=false;reliableMarginRightVal=true;if(window.getComputedStyle){pixelPositionVal=(window.getComputedStyle(div,null)||{}).top!=="1%";boxSizingReliableVal=(window.getComputedStyle(div,null)||{width:"4px"}).width==="4px";contents=div.appendChild(document.createElement("div"));contents.style.cssText=div.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;"+"box-sizing:content-box;display:block;margin:0;border:0;padding:0";contents.style.marginRight=contents.style.width="0";div.style.width="1px";reliableMarginRightVal=!parseFloat((window.getComputedStyle(contents,null)||{}).marginRight);div.removeChild(contents)}div.innerHTML="
t
";contents=div.getElementsByTagName("td");contents[0].style.cssText="margin:0;border:0;padding:0;display:none";reliableHiddenOffsetsVal=contents[0].offsetHeight===0;if(reliableHiddenOffsetsVal){contents[0].style.display="";contents[1].style.display="none";reliableHiddenOffsetsVal=contents[0].offsetHeight===0}body.removeChild(container)}})();jQuery.swap=function(elem,options,callback,args){var ret,name,old={};for(name in options){old[name]=elem.style[name];elem.style[name]=options[name]}ret=callback.apply(elem,args||[]);for(name in options){elem.style[name]=old[name]}return ret};var ralpha=/alpha\([^)]*\)/i,ropacity=/opacity\s*=\s*([^)]*)/,rdisplayswap=/^(none|table(?!-c[ea]).+)/,rnumsplit=new RegExp("^("+pnum+")(.*)$","i"),rrelNum=new RegExp("^([+-])=("+pnum+")","i"),cssShow={position:"absolute",visibility:"hidden",display:"block"},cssNormalTransform={letterSpacing:"0",fontWeight:"400"},cssPrefixes=["Webkit","O","Moz","ms"];function vendorPropName(style,name){if(name in style){return name}var capName=name.charAt(0).toUpperCase()+name.slice(1),origName=name,i=cssPrefixes.length;while(i--){name=cssPrefixes[i]+capName;if(name in style){return name}}return origName}function showHide(elements,show){var display,elem,hidden,values=[],index=0,length=elements.length;for(;index=1||value==="")&&jQuery.trim(filter.replace(ralpha,""))===""&&style.removeAttribute){style.removeAttribute("filter");if(value===""||currentStyle&&!currentStyle.filter){return}}style.filter=ralpha.test(filter)?filter.replace(ralpha,opacity):filter+" "+opacity}}}jQuery.cssHooks.marginRight=addGetHookIf(support.reliableMarginRight,function(elem,computed){if(computed){return jQuery.swap(elem,{display:"inline-block"},curCSS,[elem,"marginRight"])}});jQuery.each({margin:"",padding:"",border:"Width"},function(prefix,suffix){jQuery.cssHooks[prefix+suffix]={expand:function(value){var i=0,expanded={},parts=typeof value==="string"?value.split(" "):[value];for(;i<4;i++){expanded[prefix+cssExpand[i]+suffix]=parts[i]||parts[i-2]||parts[0]}return expanded}};if(!rmargin.test(prefix)){jQuery.cssHooks[prefix+suffix].set=setPositiveNumber}});jQuery.fn.extend({css:function(name,value){return access(this,function(elem,name,value){var styles,len,map={},i=0;if(jQuery.isArray(name)){styles=getStyles(elem);len=name.length;for(;i1)},show:function(){return showHide(this,true)},hide:function(){return showHide(this)},toggle:function(state){if(typeof state==="boolean"){return state?this.show():this.hide()}return this.each(function(){if(isHidden(this)){jQuery(this).show()}else{jQuery(this).hide()}})}});function Tween(elem,options,prop,end,easing){return new Tween.prototype.init(elem,options,prop,end,easing)}jQuery.Tween=Tween;Tween.prototype={constructor:Tween,init:function(elem,options,prop,end,easing,unit){this.elem=elem;this.prop=prop;this.easing=easing||"swing";this.options=options;this.start=this.now=this.cur();this.end=end;this.unit=unit||(jQuery.cssNumber[prop]?"":"px")},cur:function(){var hooks=Tween.propHooks[this.prop];return hooks&&hooks.get?hooks.get(this):Tween.propHooks._default.get(this)},run:function(percent){var eased,hooks=Tween.propHooks[this.prop];if(this.options.duration){this.pos=eased=jQuery.easing[this.easing](percent,this.options.duration*percent,0,1,this.options.duration)}else{this.pos=eased=percent}this.now=(this.end-this.start)*eased+this.start;if(this.options.step){this.options.step.call(this.elem,this.now,this)}if(hooks&&hooks.set){hooks.set(this)}else{Tween.propHooks._default.set(this)}return this}};Tween.prototype.init.prototype=Tween.prototype;Tween.propHooks={_default:{get:function(tween){var result;if(tween.elem[tween.prop]!=null&&(!tween.elem.style||tween.elem.style[tween.prop]==null)){return tween.elem[tween.prop]}result=jQuery.css(tween.elem,tween.prop,"");return!result||result==="auto"?0:result},set:function(tween){if(jQuery.fx.step[tween.prop]){jQuery.fx.step[tween.prop](tween)}else if(tween.elem.style&&(tween.elem.style[jQuery.cssProps[tween.prop]]!=null||jQuery.cssHooks[tween.prop])){jQuery.style(tween.elem,tween.prop,tween.now+tween.unit)}else{tween.elem[tween.prop]=tween.now}}}};Tween.propHooks.scrollTop=Tween.propHooks.scrollLeft={set:function(tween){if(tween.elem.nodeType&&tween.elem.parentNode){tween.elem[tween.prop]=tween.now}}};jQuery.easing={linear:function(p){return p},swing:function(p){return.5-Math.cos(p*Math.PI)/2}};jQuery.fx=Tween.prototype.init;jQuery.fx.step={};var fxNow,timerId,rfxtypes=/^(?:toggle|show|hide)$/,rfxnum=new RegExp("^(?:([+-])=|)("+pnum+")([a-z%]*)$","i"),rrun=/queueHooks$/,animationPrefilters=[defaultPrefilter],tweeners={"*":[function(prop,value){var tween=this.createTween(prop,value),target=tween.cur(),parts=rfxnum.exec(value),unit=parts&&parts[3]||(jQuery.cssNumber[prop]?"":"px"),start=(jQuery.cssNumber[prop]||unit!=="px"&&+target)&&rfxnum.exec(jQuery.css(tween.elem,prop)),scale=1,maxIterations=20;if(start&&start[3]!==unit){unit=unit||start[3];parts=parts||[];start=+target||1;do{scale=scale||".5";start=start/scale;jQuery.style(tween.elem,prop,start+unit)}while(scale!==(scale=tween.cur()/target)&&scale!==1&&--maxIterations)}if(parts){start=tween.start=+start||+target||0;tween.unit=unit;tween.end=parts[1]?start+(parts[1]+1)*parts[2]:+parts[2]}return tween}]};function createFxNow(){setTimeout(function(){fxNow=undefined});return fxNow=jQuery.now()}function genFx(type,includeWidth){var which,attrs={height:type},i=0;includeWidth=includeWidth?1:0;for(;i<4;i+=2-includeWidth){which=cssExpand[i];attrs["margin"+which]=attrs["padding"+which]=type}if(includeWidth){attrs.opacity=attrs.width=type}return attrs}function createTween(value,prop,animation){var tween,collection=(tweeners[prop]||[]).concat(tweeners["*"]),index=0,length=collection.length;for(;index
a";a=div.getElementsByTagName("a")[0];select=document.createElement("select");opt=select.appendChild(document.createElement("option"));input=div.getElementsByTagName("input")[0];a.style.cssText="top:1px";support.getSetAttribute=div.className!=="t";support.style=/top/.test(a.getAttribute("style"));support.hrefNormalized=a.getAttribute("href")==="/a";support.checkOn=!!input.value;support.optSelected=opt.selected;support.enctype=!!document.createElement("form").enctype;select.disabled=true;support.optDisabled=!opt.disabled;input=document.createElement("input");input.setAttribute("value","");support.input=input.getAttribute("value")==="";input.value="t";input.setAttribute("type","radio");support.radioValue=input.value==="t"})();var rreturn=/\r/g;jQuery.fn.extend({val:function(value){var hooks,ret,isFunction,elem=this[0];if(!arguments.length){if(elem){hooks=jQuery.valHooks[elem.type]||jQuery.valHooks[elem.nodeName.toLowerCase()];if(hooks&&"get"in hooks&&(ret=hooks.get(elem,"value"))!==undefined){return ret}ret=elem.value;return typeof ret==="string"?ret.replace(rreturn,""):ret==null?"":ret}return}isFunction=jQuery.isFunction(value);return this.each(function(i){var val;if(this.nodeType!==1){return}if(isFunction){val=value.call(this,i,jQuery(this).val())}else{val=value}if(val==null){val=""}else if(typeof val==="number"){val+=""}else if(jQuery.isArray(val)){val=jQuery.map(val,function(value){return value==null?"":value+""})}hooks=jQuery.valHooks[this.type]||jQuery.valHooks[this.nodeName.toLowerCase()];if(!hooks||!("set"in hooks)||hooks.set(this,val,"value")===undefined){this.value=val}})}});jQuery.extend({valHooks:{option:{get:function(elem){var val=jQuery.find.attr(elem,"value");return val!=null?val:jQuery.trim(jQuery.text(elem))}},select:{get:function(elem){var value,option,options=elem.options,index=elem.selectedIndex,one=elem.type==="select-one"||index<0,values=one?null:[],max=one?index+1:options.length,i=index<0?max:one?index:0;for(;i=0){try{option.selected=optionSet=true}catch(_){option.scrollHeight}}else{option.selected=false}}if(!optionSet){elem.selectedIndex=-1}return options}}}});jQuery.each(["radio","checkbox"],function(){jQuery.valHooks[this]={set:function(elem,value){if(jQuery.isArray(value)){return elem.checked=jQuery.inArray(jQuery(elem).val(),value)>=0}}};if(!support.checkOn){jQuery.valHooks[this].get=function(elem){return elem.getAttribute("value")===null?"on":elem.value}}});var nodeHook,boolHook,attrHandle=jQuery.expr.attrHandle,ruseDefault=/^(?:checked|selected)$/i,getSetAttribute=support.getSetAttribute,getSetInput=support.input;jQuery.fn.extend({attr:function(name,value){return access(this,jQuery.attr,name,value,arguments.length>1)},removeAttr:function(name){return this.each(function(){jQuery.removeAttr(this,name)})}});jQuery.extend({attr:function(elem,name,value){var hooks,ret,nType=elem.nodeType;if(!elem||nType===3||nType===8||nType===2){return}if(typeof elem.getAttribute===strundefined){return jQuery.prop(elem,name,value)}if(nType!==1||!jQuery.isXMLDoc(elem)){name=name.toLowerCase();hooks=jQuery.attrHooks[name]||(jQuery.expr.match.bool.test(name)?boolHook:nodeHook)}if(value!==undefined){if(value===null){jQuery.removeAttr(elem,name)}else if(hooks&&"set"in hooks&&(ret=hooks.set(elem,value,name))!==undefined){return ret}else{elem.setAttribute(name,value+"");return value}}else if(hooks&&"get"in hooks&&(ret=hooks.get(elem,name))!==null){return ret}else{ret=jQuery.find.attr(elem,name);return ret==null?undefined:ret}},removeAttr:function(elem,value){var name,propName,i=0,attrNames=value&&value.match(rnotwhite);if(attrNames&&elem.nodeType===1){while(name=attrNames[i++]){propName=jQuery.propFix[name]||name;if(jQuery.expr.match.bool.test(name)){if(getSetInput&&getSetAttribute||!ruseDefault.test(name)){elem[propName]=false}else{elem[jQuery.camelCase("default-"+name)]=elem[propName]=false}}else{jQuery.attr(elem,name,"")}elem.removeAttribute(getSetAttribute?name:propName)}}},attrHooks:{type:{set:function(elem,value){if(!support.radioValue&&value==="radio"&&jQuery.nodeName(elem,"input")){var val=elem.value;elem.setAttribute("type",value);if(val){elem.value=val}return value}}}}});boolHook={set:function(elem,value,name){if(value===false){jQuery.removeAttr(elem,name)}else if(getSetInput&&getSetAttribute||!ruseDefault.test(name)){elem.setAttribute(!getSetAttribute&&jQuery.propFix[name]||name,name)}else{elem[jQuery.camelCase("default-"+name)]=elem[name]=true}return name}};jQuery.each(jQuery.expr.match.bool.source.match(/\w+/g),function(i,name){var getter=attrHandle[name]||jQuery.find.attr;attrHandle[name]=getSetInput&&getSetAttribute||!ruseDefault.test(name)?function(elem,name,isXML){var ret,handle;if(!isXML){handle=attrHandle[name];attrHandle[name]=ret;ret=getter(elem,name,isXML)!=null?name.toLowerCase():null;attrHandle[name]=handle}return ret}:function(elem,name,isXML){if(!isXML){return elem[jQuery.camelCase("default-"+name)]?name.toLowerCase():null}}});if(!getSetInput||!getSetAttribute){jQuery.attrHooks.value={set:function(elem,value,name){if(jQuery.nodeName(elem,"input")){elem.defaultValue=value}else{return nodeHook&&nodeHook.set(elem,value,name)}}}}if(!getSetAttribute){nodeHook={set:function(elem,value,name){var ret=elem.getAttributeNode(name);if(!ret){elem.setAttributeNode(ret=elem.ownerDocument.createAttribute(name))}ret.value=value+="";if(name==="value"||value===elem.getAttribute(name)){return value}}};attrHandle.id=attrHandle.name=attrHandle.coords=function(elem,name,isXML){var ret;if(!isXML){return(ret=elem.getAttributeNode(name))&&ret.value!==""?ret.value:null}};jQuery.valHooks.button={get:function(elem,name){var ret=elem.getAttributeNode(name);if(ret&&ret.specified){return ret.value}},set:nodeHook.set};jQuery.attrHooks.contenteditable={set:function(elem,value,name){nodeHook.set(elem,value===""?false:value,name)}};jQuery.each(["width","height"],function(i,name){jQuery.attrHooks[name]={set:function(elem,value){if(value===""){elem.setAttribute(name,"auto");return value}}}})}if(!support.style){jQuery.attrHooks.style={get:function(elem){return elem.style.cssText||undefined},set:function(elem,value){return elem.style.cssText=value+""}}}var rfocusable=/^(?:input|select|textarea|button|object)$/i,rclickable=/^(?:a|area)$/i;jQuery.fn.extend({prop:function(name,value){return access(this,jQuery.prop,name,value,arguments.length>1)},removeProp:function(name){name=jQuery.propFix[name]||name;return this.each(function(){try{this[name]=undefined;delete this[name]}catch(e){}})}});jQuery.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(elem,name,value){var ret,hooks,notxml,nType=elem.nodeType;if(!elem||nType===3||nType===8||nType===2){return}notxml=nType!==1||!jQuery.isXMLDoc(elem);if(notxml){name=jQuery.propFix[name]||name;hooks=jQuery.propHooks[name]}if(value!==undefined){return hooks&&"set"in hooks&&(ret=hooks.set(elem,value,name))!==undefined?ret:elem[name]=value}else{return hooks&&"get"in hooks&&(ret=hooks.get(elem,name))!==null?ret:elem[name]}},propHooks:{tabIndex:{get:function(elem){var tabindex=jQuery.find.attr(elem,"tabindex");return tabindex?parseInt(tabindex,10):rfocusable.test(elem.nodeName)||rclickable.test(elem.nodeName)&&elem.href?0:-1}}}});if(!support.hrefNormalized){jQuery.each(["href","src"],function(i,name){jQuery.propHooks[name]={get:function(elem){return elem.getAttribute(name,4)}}})}if(!support.optSelected){jQuery.propHooks.selected={get:function(elem){var parent=elem.parentNode;if(parent){parent.selectedIndex;if(parent.parentNode){parent.parentNode.selectedIndex}}return null}}}jQuery.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){jQuery.propFix[this.toLowerCase()]=this});if(!support.enctype){jQuery.propFix.enctype="encoding"}var rclass=/[\t\r\n\f]/g;jQuery.fn.extend({addClass:function(value){var classes,elem,cur,clazz,j,finalValue,i=0,len=this.length,proceed=typeof value==="string"&&value;if(jQuery.isFunction(value)){return this.each(function(j){jQuery(this).addClass(value.call(this,j,this.className))})}if(proceed){classes=(value||"").match(rnotwhite)||[];for(;i=0){cur=cur.replace(" "+clazz+" "," ")}}finalValue=value?jQuery.trim(cur):"";if(elem.className!==finalValue){elem.className=finalValue}}}}return this},toggleClass:function(value,stateVal){var type=typeof value;if(typeof stateVal==="boolean"&&type==="string"){return stateVal?this.addClass(value):this.removeClass(value)}if(jQuery.isFunction(value)){return this.each(function(i){jQuery(this).toggleClass(value.call(this,i,this.className,stateVal),stateVal)})}return this.each(function(){if(type==="string"){var className,i=0,self=jQuery(this),classNames=value.match(rnotwhite)||[];while(className=classNames[i++]){if(self.hasClass(className)){self.removeClass(className)}else{self.addClass(className)}}}else if(type===strundefined||type==="boolean"){if(this.className){jQuery._data(this,"__className__",this.className)}this.className=this.className||value===false?"":jQuery._data(this,"__className__")||""}})},hasClass:function(selector){var className=" "+selector+" ",i=0,l=this.length;for(;i=0){return true}}return false}});jQuery.each(("blur focus focusin focusout load resize scroll unload click dblclick "+"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave "+"change select submit keydown keypress keyup error contextmenu").split(" "),function(i,name){jQuery.fn[name]=function(data,fn){return arguments.length>0?this.on(name,null,data,fn):this.trigger(name)}});jQuery.fn.extend({hover:function(fnOver,fnOut){return this.mouseenter(fnOver).mouseleave(fnOut||fnOver)},bind:function(types,data,fn){return this.on(types,null,data,fn)},unbind:function(types,fn){return this.off(types,null,fn)},delegate:function(selector,types,data,fn){return this.on(types,selector,data,fn)},undelegate:function(selector,types,fn){return arguments.length===1?this.off(selector,"**"):this.off(types,selector||"**",fn)}});var nonce=jQuery.now();var rquery=/\?/;var rvalidtokens=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;jQuery.parseJSON=function(data){if(window.JSON&&window.JSON.parse){return window.JSON.parse(data+"")}var requireNonComma,depth=null,str=jQuery.trim(data+"");return str&&!jQuery.trim(str.replace(rvalidtokens,function(token,comma,open,close){if(requireNonComma&&comma){depth=0}if(depth===0){return token}requireNonComma=open||comma;depth+=!close-!open;return""}))?Function("return "+str)():jQuery.error("Invalid JSON: "+data)};jQuery.parseXML=function(data){var xml,tmp;if(!data||typeof data!=="string"){return null}try{if(window.DOMParser){tmp=new DOMParser;xml=tmp.parseFromString(data,"text/xml")}else{xml=new ActiveXObject("Microsoft.XMLDOM");xml.async="false";xml.loadXML(data)}}catch(e){xml=undefined}if(!xml||!xml.documentElement||xml.getElementsByTagName("parsererror").length){jQuery.error("Invalid XML: "+data)}return xml};var ajaxLocParts,ajaxLocation,rhash=/#.*$/,rts=/([?&])_=[^&]*/,rheaders=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,rlocalProtocol=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,rnoContent=/^(?:GET|HEAD)$/,rprotocol=/^\/\//,rurl=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,prefilters={},transports={},allTypes="*/".concat("*");try{ajaxLocation=location.href}catch(e){ajaxLocation=document.createElement("a");ajaxLocation.href="";ajaxLocation=ajaxLocation.href}ajaxLocParts=rurl.exec(ajaxLocation.toLowerCase())||[];function addToPrefiltersOrTransports(structure){return function(dataTypeExpression,func){if(typeof dataTypeExpression!=="string"){func=dataTypeExpression;dataTypeExpression="*"}var dataType,i=0,dataTypes=dataTypeExpression.toLowerCase().match(rnotwhite)||[];if(jQuery.isFunction(func)){while(dataType=dataTypes[i++]){if(dataType.charAt(0)==="+"){dataType=dataType.slice(1)||"*";(structure[dataType]=structure[dataType]||[]).unshift(func)}else{(structure[dataType]=structure[dataType]||[]).push(func)}}}}}function inspectPrefiltersOrTransports(structure,options,originalOptions,jqXHR){var inspected={},seekingTransport=structure===transports;function inspect(dataType){var selected;inspected[dataType]=true;jQuery.each(structure[dataType]||[],function(_,prefilterOrFactory){var dataTypeOrTransport=prefilterOrFactory(options,originalOptions,jqXHR);if(typeof dataTypeOrTransport==="string"&&!seekingTransport&&!inspected[dataTypeOrTransport]){options.dataTypes.unshift(dataTypeOrTransport);inspect(dataTypeOrTransport);return false}else if(seekingTransport){return!(selected=dataTypeOrTransport)}});return selected}return inspect(options.dataTypes[0])||!inspected["*"]&&inspect("*")}function ajaxExtend(target,src){var deep,key,flatOptions=jQuery.ajaxSettings.flatOptions||{};for(key in src){if(src[key]!==undefined){(flatOptions[key]?target:deep||(deep={}))[key]=src[key]}}if(deep){jQuery.extend(true,target,deep)}return target}function ajaxHandleResponses(s,jqXHR,responses){var firstDataType,ct,finalDataType,type,contents=s.contents,dataTypes=s.dataTypes;while(dataTypes[0]==="*"){dataTypes.shift();if(ct===undefined){ct=s.mimeType||jqXHR.getResponseHeader("Content-Type")}}if(ct){for(type in contents){if(contents[type]&&contents[type].test(ct)){dataTypes.unshift(type);break}}}if(dataTypes[0]in responses){finalDataType=dataTypes[0]}else{for(type in responses){if(!dataTypes[0]||s.converters[type+" "+dataTypes[0]]){finalDataType=type;break}if(!firstDataType){firstDataType=type}}finalDataType=finalDataType||firstDataType}if(finalDataType){if(finalDataType!==dataTypes[0]){dataTypes.unshift(finalDataType)}return responses[finalDataType]}}function ajaxConvert(s,response,jqXHR,isSuccess){var conv2,current,conv,tmp,prev,converters={},dataTypes=s.dataTypes.slice();if(dataTypes[1]){for(conv in s.converters){converters[conv.toLowerCase()]=s.converters[conv]}}current=dataTypes.shift();while(current){if(s.responseFields[current]){jqXHR[s.responseFields[current]]=response}if(!prev&&isSuccess&&s.dataFilter){response=s.dataFilter(response,s.dataType)}prev=current;current=dataTypes.shift();if(current){if(current==="*"){current=prev}else if(prev!=="*"&&prev!==current){conv=converters[prev+" "+current]||converters["* "+current];if(!conv){for(conv2 in converters){tmp=conv2.split(" ");if(tmp[1]===current){conv=converters[prev+" "+tmp[0]]||converters["* "+tmp[0]];if(conv){if(conv===true){conv=converters[conv2]}else if(converters[conv2]!==true){current=tmp[0];dataTypes.unshift(tmp[1])}break}}}}if(conv!==true){if(conv&&s["throws"]){response=conv(response)}else{try{response=conv(response)}catch(e){return{state:"parsererror",error:conv?e:"No conversion from "+prev+" to "+current}}}}}}}return{state:"success",data:response}}jQuery.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:ajaxLocation,type:"GET",isLocal:rlocalProtocol.test(ajaxLocParts[1]),global:true,processData:true,async:true,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":allTypes,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":true,"text json":jQuery.parseJSON,"text xml":jQuery.parseXML},flatOptions:{url:true,context:true}},ajaxSetup:function(target,settings){return settings?ajaxExtend(ajaxExtend(target,jQuery.ajaxSettings),settings):ajaxExtend(jQuery.ajaxSettings,target)},ajaxPrefilter:addToPrefiltersOrTransports(prefilters),ajaxTransport:addToPrefiltersOrTransports(transports),ajax:function(url,options){if(typeof url==="object"){options=url; -url=undefined}options=options||{};var parts,i,cacheURL,responseHeadersString,timeoutTimer,fireGlobals,transport,responseHeaders,s=jQuery.ajaxSetup({},options),callbackContext=s.context||s,globalEventContext=s.context&&(callbackContext.nodeType||callbackContext.jquery)?jQuery(callbackContext):jQuery.event,deferred=jQuery.Deferred(),completeDeferred=jQuery.Callbacks("once memory"),statusCode=s.statusCode||{},requestHeaders={},requestHeadersNames={},state=0,strAbort="canceled",jqXHR={readyState:0,getResponseHeader:function(key){var match;if(state===2){if(!responseHeaders){responseHeaders={};while(match=rheaders.exec(responseHeadersString)){responseHeaders[match[1].toLowerCase()]=match[2]}}match=responseHeaders[key.toLowerCase()]}return match==null?null:match},getAllResponseHeaders:function(){return state===2?responseHeadersString:null},setRequestHeader:function(name,value){var lname=name.toLowerCase();if(!state){name=requestHeadersNames[lname]=requestHeadersNames[lname]||name;requestHeaders[name]=value}return this},overrideMimeType:function(type){if(!state){s.mimeType=type}return this},statusCode:function(map){var code;if(map){if(state<2){for(code in map){statusCode[code]=[statusCode[code],map[code]]}}else{jqXHR.always(map[jqXHR.status])}}return this},abort:function(statusText){var finalText=statusText||strAbort;if(transport){transport.abort(finalText)}done(0,finalText);return this}};deferred.promise(jqXHR).complete=completeDeferred.add;jqXHR.success=jqXHR.done;jqXHR.error=jqXHR.fail;s.url=((url||s.url||ajaxLocation)+"").replace(rhash,"").replace(rprotocol,ajaxLocParts[1]+"//");s.type=options.method||options.type||s.method||s.type;s.dataTypes=jQuery.trim(s.dataType||"*").toLowerCase().match(rnotwhite)||[""];if(s.crossDomain==null){parts=rurl.exec(s.url.toLowerCase());s.crossDomain=!!(parts&&(parts[1]!==ajaxLocParts[1]||parts[2]!==ajaxLocParts[2]||(parts[3]||(parts[1]==="http:"?"80":"443"))!==(ajaxLocParts[3]||(ajaxLocParts[1]==="http:"?"80":"443"))))}if(s.data&&s.processData&&typeof s.data!=="string"){s.data=jQuery.param(s.data,s.traditional)}inspectPrefiltersOrTransports(prefilters,s,options,jqXHR);if(state===2){return jqXHR}fireGlobals=jQuery.event&&s.global;if(fireGlobals&&jQuery.active++===0){jQuery.event.trigger("ajaxStart")}s.type=s.type.toUpperCase();s.hasContent=!rnoContent.test(s.type);cacheURL=s.url;if(!s.hasContent){if(s.data){cacheURL=s.url+=(rquery.test(cacheURL)?"&":"?")+s.data;delete s.data}if(s.cache===false){s.url=rts.test(cacheURL)?cacheURL.replace(rts,"$1_="+nonce++):cacheURL+(rquery.test(cacheURL)?"&":"?")+"_="+nonce++}}if(s.ifModified){if(jQuery.lastModified[cacheURL]){jqXHR.setRequestHeader("If-Modified-Since",jQuery.lastModified[cacheURL])}if(jQuery.etag[cacheURL]){jqXHR.setRequestHeader("If-None-Match",jQuery.etag[cacheURL])}}if(s.data&&s.hasContent&&s.contentType!==false||options.contentType){jqXHR.setRequestHeader("Content-Type",s.contentType)}jqXHR.setRequestHeader("Accept",s.dataTypes[0]&&s.accepts[s.dataTypes[0]]?s.accepts[s.dataTypes[0]]+(s.dataTypes[0]!=="*"?", "+allTypes+"; q=0.01":""):s.accepts["*"]);for(i in s.headers){jqXHR.setRequestHeader(i,s.headers[i])}if(s.beforeSend&&(s.beforeSend.call(callbackContext,jqXHR,s)===false||state===2)){return jqXHR.abort()}strAbort="abort";for(i in{success:1,error:1,complete:1}){jqXHR[i](s[i])}transport=inspectPrefiltersOrTransports(transports,s,options,jqXHR);if(!transport){done(-1,"No Transport")}else{jqXHR.readyState=1;if(fireGlobals){globalEventContext.trigger("ajaxSend",[jqXHR,s])}if(s.async&&s.timeout>0){timeoutTimer=setTimeout(function(){jqXHR.abort("timeout")},s.timeout)}try{state=1;transport.send(requestHeaders,done)}catch(e){if(state<2){done(-1,e)}else{throw e}}}function done(status,nativeStatusText,responses,headers){var isSuccess,success,error,response,modified,statusText=nativeStatusText;if(state===2){return}state=2;if(timeoutTimer){clearTimeout(timeoutTimer)}transport=undefined;responseHeadersString=headers||"";jqXHR.readyState=status>0?4:0;isSuccess=status>=200&&status<300||status===304;if(responses){response=ajaxHandleResponses(s,jqXHR,responses)}response=ajaxConvert(s,response,jqXHR,isSuccess);if(isSuccess){if(s.ifModified){modified=jqXHR.getResponseHeader("Last-Modified");if(modified){jQuery.lastModified[cacheURL]=modified}modified=jqXHR.getResponseHeader("etag");if(modified){jQuery.etag[cacheURL]=modified}}if(status===204||s.type==="HEAD"){statusText="nocontent"}else if(status===304){statusText="notmodified"}else{statusText=response.state;success=response.data;error=response.error;isSuccess=!error}}else{error=statusText;if(status||!statusText){statusText="error";if(status<0){status=0}}}jqXHR.status=status;jqXHR.statusText=(nativeStatusText||statusText)+"";if(isSuccess){deferred.resolveWith(callbackContext,[success,statusText,jqXHR])}else{deferred.rejectWith(callbackContext,[jqXHR,statusText,error])}jqXHR.statusCode(statusCode);statusCode=undefined;if(fireGlobals){globalEventContext.trigger(isSuccess?"ajaxSuccess":"ajaxError",[jqXHR,s,isSuccess?success:error])}completeDeferred.fireWith(callbackContext,[jqXHR,statusText]);if(fireGlobals){globalEventContext.trigger("ajaxComplete",[jqXHR,s]);if(!--jQuery.active){jQuery.event.trigger("ajaxStop")}}}return jqXHR},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json")},getScript:function(url,callback){return jQuery.get(url,undefined,callback,"script")}});jQuery.each(["get","post"],function(i,method){jQuery[method]=function(url,data,callback,type){if(jQuery.isFunction(data)){type=type||callback;callback=data;data=undefined}return jQuery.ajax({url:url,type:method,dataType:type,data:data,success:callback})}});jQuery._evalUrl=function(url){return jQuery.ajax({url:url,type:"GET",dataType:"script",async:false,global:false,"throws":true})};jQuery.fn.extend({wrapAll:function(html){if(jQuery.isFunction(html)){return this.each(function(i){jQuery(this).wrapAll(html.call(this,i))})}if(this[0]){var wrap=jQuery(html,this[0].ownerDocument).eq(0).clone(true);if(this[0].parentNode){wrap.insertBefore(this[0])}wrap.map(function(){var elem=this;while(elem.firstChild&&elem.firstChild.nodeType===1){elem=elem.firstChild}return elem}).append(this)}return this},wrapInner:function(html){if(jQuery.isFunction(html)){return this.each(function(i){jQuery(this).wrapInner(html.call(this,i))})}return this.each(function(){var self=jQuery(this),contents=self.contents();if(contents.length){contents.wrapAll(html)}else{self.append(html)}})},wrap:function(html){var isFunction=jQuery.isFunction(html);return this.each(function(i){jQuery(this).wrapAll(isFunction?html.call(this,i):html)})},unwrap:function(){return this.parent().each(function(){if(!jQuery.nodeName(this,"body")){jQuery(this).replaceWith(this.childNodes)}}).end()}});jQuery.expr.filters.hidden=function(elem){return elem.offsetWidth<=0&&elem.offsetHeight<=0||!support.reliableHiddenOffsets()&&(elem.style&&elem.style.display||jQuery.css(elem,"display"))==="none"};jQuery.expr.filters.visible=function(elem){return!jQuery.expr.filters.hidden(elem)};var r20=/%20/g,rbracket=/\[\]$/,rCRLF=/\r?\n/g,rsubmitterTypes=/^(?:submit|button|image|reset|file)$/i,rsubmittable=/^(?:input|select|textarea|keygen)/i;function buildParams(prefix,obj,traditional,add){var name;if(jQuery.isArray(obj)){jQuery.each(obj,function(i,v){if(traditional||rbracket.test(prefix)){add(prefix,v)}else{buildParams(prefix+"["+(typeof v==="object"?i:"")+"]",v,traditional,add)}})}else if(!traditional&&jQuery.type(obj)==="object"){for(name in obj){buildParams(prefix+"["+name+"]",obj[name],traditional,add)}}else{add(prefix,obj)}}jQuery.param=function(a,traditional){var prefix,s=[],add=function(key,value){value=jQuery.isFunction(value)?value():value==null?"":value;s[s.length]=encodeURIComponent(key)+"="+encodeURIComponent(value)};if(traditional===undefined){traditional=jQuery.ajaxSettings&&jQuery.ajaxSettings.traditional}if(jQuery.isArray(a)||a.jquery&&!jQuery.isPlainObject(a)){jQuery.each(a,function(){add(this.name,this.value)})}else{for(prefix in a){buildParams(prefix,a[prefix],traditional,add)}}return s.join("&").replace(r20,"+")};jQuery.fn.extend({serialize:function(){return jQuery.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var elements=jQuery.prop(this,"elements");return elements?jQuery.makeArray(elements):this}).filter(function(){var type=this.type;return this.name&&!jQuery(this).is(":disabled")&&rsubmittable.test(this.nodeName)&&!rsubmitterTypes.test(type)&&(this.checked||!rcheckableType.test(type))}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:jQuery.isArray(val)?jQuery.map(val,function(val){return{name:elem.name,value:val.replace(rCRLF,"\r\n")}}):{name:elem.name,value:val.replace(rCRLF,"\r\n")}}).get()}});jQuery.ajaxSettings.xhr=window.ActiveXObject!==undefined?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&createStandardXHR()||createActiveXHR()}:createStandardXHR;var xhrId=0,xhrCallbacks={},xhrSupported=jQuery.ajaxSettings.xhr();if(window.attachEvent){window.attachEvent("onunload",function(){for(var key in xhrCallbacks){xhrCallbacks[key](undefined,true)}})}support.cors=!!xhrSupported&&"withCredentials"in xhrSupported;xhrSupported=support.ajax=!!xhrSupported;if(xhrSupported){jQuery.ajaxTransport(function(options){if(!options.crossDomain||support.cors){var callback;return{send:function(headers,complete){var i,xhr=options.xhr(),id=++xhrId;xhr.open(options.type,options.url,options.async,options.username,options.password);if(options.xhrFields){for(i in options.xhrFields){xhr[i]=options.xhrFields[i]}}if(options.mimeType&&xhr.overrideMimeType){xhr.overrideMimeType(options.mimeType)}if(!options.crossDomain&&!headers["X-Requested-With"]){headers["X-Requested-With"]="XMLHttpRequest"}for(i in headers){if(headers[i]!==undefined){xhr.setRequestHeader(i,headers[i]+"")}}xhr.send(options.hasContent&&options.data||null);callback=function(_,isAbort){var status,statusText,responses;if(callback&&(isAbort||xhr.readyState===4)){delete xhrCallbacks[id];callback=undefined;xhr.onreadystatechange=jQuery.noop;if(isAbort){if(xhr.readyState!==4){xhr.abort()}}else{responses={};status=xhr.status;if(typeof xhr.responseText==="string"){responses.text=xhr.responseText}try{statusText=xhr.statusText}catch(e){statusText=""}if(!status&&options.isLocal&&!options.crossDomain){status=responses.text?200:404}else if(status===1223){status=204}}}if(responses){complete(status,statusText,responses,xhr.getAllResponseHeaders())}};if(!options.async){callback()}else if(xhr.readyState===4){setTimeout(callback)}else{xhr.onreadystatechange=xhrCallbacks[id]=callback}},abort:function(){if(callback){callback(undefined,true)}}}}})}function createStandardXHR(){try{return new window.XMLHttpRequest}catch(e){}}function createActiveXHR(){try{return new window.ActiveXObject("Microsoft.XMLHTTP")}catch(e){}}jQuery.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(text){jQuery.globalEval(text);return text}}});jQuery.ajaxPrefilter("script",function(s){if(s.cache===undefined){s.cache=false}if(s.crossDomain){s.type="GET";s.global=false}});jQuery.ajaxTransport("script",function(s){if(s.crossDomain){var script,head=document.head||jQuery("head")[0]||document.documentElement;return{send:function(_,callback){script=document.createElement("script");script.async=true;if(s.scriptCharset){script.charset=s.scriptCharset}script.src=s.url;script.onload=script.onreadystatechange=function(_,isAbort){if(isAbort||!script.readyState||/loaded|complete/.test(script.readyState)){script.onload=script.onreadystatechange=null;if(script.parentNode){script.parentNode.removeChild(script)}script=null;if(!isAbort){callback(200,"success")}}};head.insertBefore(script,head.firstChild)},abort:function(){if(script){script.onload(undefined,true)}}}}});var oldCallbacks=[],rjsonp=/(=)\?(?=&|$)|\?\?/;jQuery.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var callback=oldCallbacks.pop()||jQuery.expando+"_"+nonce++;this[callback]=true;return callback}});jQuery.ajaxPrefilter("json jsonp",function(s,originalSettings,jqXHR){var callbackName,overwritten,responseContainer,jsonProp=s.jsonp!==false&&(rjsonp.test(s.url)?"url":typeof s.data==="string"&&!(s.contentType||"").indexOf("application/x-www-form-urlencoded")&&rjsonp.test(s.data)&&"data");if(jsonProp||s.dataTypes[0]==="jsonp"){callbackName=s.jsonpCallback=jQuery.isFunction(s.jsonpCallback)?s.jsonpCallback():s.jsonpCallback;if(jsonProp){s[jsonProp]=s[jsonProp].replace(rjsonp,"$1"+callbackName)}else if(s.jsonp!==false){s.url+=(rquery.test(s.url)?"&":"?")+s.jsonp+"="+callbackName}s.converters["script json"]=function(){if(!responseContainer){jQuery.error(callbackName+" was not called")}return responseContainer[0]};s.dataTypes[0]="json";overwritten=window[callbackName];window[callbackName]=function(){responseContainer=arguments};jqXHR.always(function(){window[callbackName]=overwritten;if(s[callbackName]){s.jsonpCallback=originalSettings.jsonpCallback;oldCallbacks.push(callbackName)}if(responseContainer&&jQuery.isFunction(overwritten)){overwritten(responseContainer[0])}responseContainer=overwritten=undefined});return"script"}});jQuery.parseHTML=function(data,context,keepScripts){if(!data||typeof data!=="string"){return null}if(typeof context==="boolean"){keepScripts=context;context=false}context=context||document;var parsed=rsingleTag.exec(data),scripts=!keepScripts&&[];if(parsed){return[context.createElement(parsed[1])]}parsed=jQuery.buildFragment([data],context,scripts);if(scripts&&scripts.length){jQuery(scripts).remove()}return jQuery.merge([],parsed.childNodes)};var _load=jQuery.fn.load;jQuery.fn.load=function(url,params,callback){if(typeof url!=="string"&&_load){return _load.apply(this,arguments)}var selector,response,type,self=this,off=url.indexOf(" ");if(off>=0){selector=jQuery.trim(url.slice(off,url.length));url=url.slice(0,off)}if(jQuery.isFunction(params)){callback=params;params=undefined}else if(params&&typeof params==="object"){type="POST"}if(self.length>0){jQuery.ajax({url:url,type:type,dataType:"html",data:params}).done(function(responseText){response=arguments;self.html(selector?jQuery("
").append(jQuery.parseHTML(responseText)).find(selector):responseText)}).complete(callback&&function(jqXHR,status){self.each(callback,response||[jqXHR.responseText,status,jqXHR])})}return this};jQuery.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(i,type){jQuery.fn[type]=function(fn){return this.on(type,fn)}});jQuery.expr.filters.animated=function(elem){return jQuery.grep(jQuery.timers,function(fn){return elem===fn.elem}).length};var docElem=window.document.documentElement;function getWindow(elem){return jQuery.isWindow(elem)?elem:elem.nodeType===9?elem.defaultView||elem.parentWindow:false}jQuery.offset={setOffset:function(elem,options,i){var curPosition,curLeft,curCSSTop,curTop,curOffset,curCSSLeft,calculatePosition,position=jQuery.css(elem,"position"),curElem=jQuery(elem),props={};if(position==="static"){elem.style.position="relative"}curOffset=curElem.offset();curCSSTop=jQuery.css(elem,"top");curCSSLeft=jQuery.css(elem,"left");calculatePosition=(position==="absolute"||position==="fixed")&&jQuery.inArray("auto",[curCSSTop,curCSSLeft])>-1;if(calculatePosition){curPosition=curElem.position();curTop=curPosition.top;curLeft=curPosition.left}else{curTop=parseFloat(curCSSTop)||0;curLeft=parseFloat(curCSSLeft)||0}if(jQuery.isFunction(options)){options=options.call(elem,i,curOffset)}if(options.top!=null){props.top=options.top-curOffset.top+curTop}if(options.left!=null){props.left=options.left-curOffset.left+curLeft}if("using"in options){options.using.call(elem,props)}else{curElem.css(props)}}};jQuery.fn.extend({offset:function(options){if(arguments.length){return options===undefined?this:this.each(function(i){jQuery.offset.setOffset(this,options,i)})}var docElem,win,box={top:0,left:0},elem=this[0],doc=elem&&elem.ownerDocument;if(!doc){return}docElem=doc.documentElement;if(!jQuery.contains(docElem,elem)){return box}if(typeof elem.getBoundingClientRect!==strundefined){box=elem.getBoundingClientRect()}win=getWindow(doc);return{top:box.top+(win.pageYOffset||docElem.scrollTop)-(docElem.clientTop||0),left:box.left+(win.pageXOffset||docElem.scrollLeft)-(docElem.clientLeft||0)}},position:function(){if(!this[0]){return}var offsetParent,offset,parentOffset={top:0,left:0},elem=this[0];if(jQuery.css(elem,"position")==="fixed"){offset=elem.getBoundingClientRect()}else{offsetParent=this.offsetParent();offset=this.offset();if(!jQuery.nodeName(offsetParent[0],"html")){parentOffset=offsetParent.offset()}parentOffset.top+=jQuery.css(offsetParent[0],"borderTopWidth",true);parentOffset.left+=jQuery.css(offsetParent[0],"borderLeftWidth",true)}return{top:offset.top-parentOffset.top-jQuery.css(elem,"marginTop",true),left:offset.left-parentOffset.left-jQuery.css(elem,"marginLeft",true)}},offsetParent:function(){return this.map(function(){var offsetParent=this.offsetParent||docElem;while(offsetParent&&(!jQuery.nodeName(offsetParent,"html")&&jQuery.css(offsetParent,"position")==="static")){offsetParent=offsetParent.offsetParent}return offsetParent||docElem})}});jQuery.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(method,prop){var top=/Y/.test(prop);jQuery.fn[method]=function(val){return access(this,function(elem,method,val){var win=getWindow(elem);if(val===undefined){return win?prop in win?win[prop]:win.document.documentElement[method]:elem[method]}if(win){win.scrollTo(!top?val:jQuery(win).scrollLeft(),top?val:jQuery(win).scrollTop())}else{elem[method]=val}},method,val,arguments.length,null)}});jQuery.each(["top","left"],function(i,prop){jQuery.cssHooks[prop]=addGetHookIf(support.pixelPosition,function(elem,computed){if(computed){computed=curCSS(elem,prop);return rnumnonpx.test(computed)?jQuery(elem).position()[prop]+"px":computed}})});jQuery.each({Height:"height",Width:"width"},function(name,type){jQuery.each({padding:"inner"+name,content:type,"":"outer"+name},function(defaultExtra,funcName){jQuery.fn[funcName]=function(margin,value){var chainable=arguments.length&&(defaultExtra||typeof margin!=="boolean"),extra=defaultExtra||(margin===true||value===true?"margin":"border");return access(this,function(elem,type,value){var doc;if(jQuery.isWindow(elem)){return elem.document.documentElement["client"+name]}if(elem.nodeType===9){doc=elem.documentElement;return Math.max(elem.body["scroll"+name],doc["scroll"+name],elem.body["offset"+name],doc["offset"+name],doc["client"+name])}return value===undefined?jQuery.css(elem,type,extra):jQuery.style(elem,type,value,extra)},type,chainable?margin:undefined,chainable,null)}})});jQuery.fn.size=function(){return this.length};jQuery.fn.andSelf=jQuery.fn.addBack;if(typeof define==="function"&&define.amd){define("jquery",[],function(){return jQuery})}var _jQuery=window.jQuery,_$=window.$;jQuery.noConflict=function(deep){if(window.$===jQuery){window.$=_$}if(deep&&window.jQuery===jQuery){window.jQuery=_jQuery}return jQuery};if(typeof noGlobal===strundefined){window.jQuery=window.$=jQuery}return jQuery}); diff --git a/htmlcov/jquery.tablesorter.min.js b/htmlcov/jquery.tablesorter.min.js deleted file mode 100644 index 64c70071..00000000 --- a/htmlcov/jquery.tablesorter.min.js +++ /dev/null @@ -1,2 +0,0 @@ - -(function($){$.extend({tablesorter:new function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'.',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}var rows=table.tBodies[0].rows;if(table.tBodies[0].rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;ib)?1:0));};function sortTextDesc(a,b){return((ba)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){$this.trigger("sortStart");var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){var $cell=$(this);var i=this.column;this.order=this.count++%2;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i5cj*13AM(ls%l5e%NL KelF{r5}E+1W**4^ diff --git a/htmlcov/keybd_open.png b/htmlcov/keybd_open.png deleted file mode 100644 index db114023f096297a23a7b1266b469d0ce4556b0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^%0SG+!2%?mw9Xg;DRWO3$B+uf5cj*13AM(ls%l5e%NL KelF{r5}E+1W**4^ diff --git a/htmlcov/status.json b/htmlcov/status.json deleted file mode 100644 index bab3cbb5..00000000 --- a/htmlcov/status.json +++ /dev/null @@ -1 +0,0 @@ -{"format":2,"version":"5.5","globals":"4484765ec61067794c2e130f1c785650","files":{"sygnal___init___py":{"hash":"077acb19083cdbcdd5c8bde09b55d413","index":{"nums":[1,5,0,0,0,0,0],"html_filename":"sygnal___init___py.html","relative_filename":"sygnal/__init__.py"}},"sygnal_apnspushkin_py":{"hash":"e20be1763a7c34c021be9c325835d038","index":{"nums":[1,236,0,76,0,0,0],"html_filename":"sygnal_apnspushkin_py.html","relative_filename":"sygnal/apnspushkin.py"}},"sygnal_apnstruncate_py":{"hash":"5579456f1172196c723dc8e9235008d7","index":{"nums":[1,65,0,5,0,0,0],"html_filename":"sygnal_apnstruncate_py.html","relative_filename":"sygnal/apnstruncate.py"}},"sygnal_exceptions_py":{"hash":"a04d59fe81ecea680f4693ba885f5a6f","index":{"nums":[1,13,0,0,0,0,0],"html_filename":"sygnal_exceptions_py.html","relative_filename":"sygnal/exceptions.py"}},"sygnal_gcmpushkin_py":{"hash":"dc4dd9993b24c4d20071e363e2fec956","index":{"nums":[1,196,0,49,0,0,0],"html_filename":"sygnal_gcmpushkin_py.html","relative_filename":"sygnal/gcmpushkin.py"}},"sygnal_helper___init___py":{"hash":"af9d0d0de0ea3b71d198bc63f1499a7d","index":{"nums":[1,0,0,0,0,0,0],"html_filename":"sygnal_helper___init___py.html","relative_filename":"sygnal/helper/__init__.py"}},"sygnal_helper_context_factory_py":{"hash":"41bad63ee7f2343e074d128d9bf55843","index":{"nums":[1,65,0,34,0,0,0],"html_filename":"sygnal_helper_context_factory_py.html","relative_filename":"sygnal/helper/context_factory.py"}},"sygnal_helper_proxy___init___py":{"hash":"788683592f439c906746b6abd0664930","index":{"nums":[1,13,0,0,0,0,0],"html_filename":"sygnal_helper_proxy___init___py.html","relative_filename":"sygnal/helper/proxy/__init__.py"}},"sygnal_helper_proxy_connectproxyclient_twisted_py":{"hash":"6fb790b3f939e293b36251b1dc000108","index":{"nums":[1,99,0,15,0,0,0],"html_filename":"sygnal_helper_proxy_connectproxyclient_twisted_py.html","relative_filename":"sygnal/helper/proxy/connectproxyclient_twisted.py"}},"sygnal_helper_proxy_proxy_asyncio_py":{"hash":"93b30bfebed8f9d19a3fc977de9a709b","index":{"nums":[1,119,0,26,0,0,0],"html_filename":"sygnal_helper_proxy_proxy_asyncio_py.html","relative_filename":"sygnal/helper/proxy/proxy_asyncio.py"}},"sygnal_helper_proxy_proxyagent_twisted_py":{"hash":"9d1c67a4f3bd4a58233a6e37df1846d3","index":{"nums":[1,53,0,10,0,0,0],"html_filename":"sygnal_helper_proxy_proxyagent_twisted_py.html","relative_filename":"sygnal/helper/proxy/proxyagent_twisted.py"}},"sygnal_http_py":{"hash":"27100333b7d8c0f1763c8ff01aa81625","index":{"nums":[1,166,0,28,0,0,0],"html_filename":"sygnal_http_py.html","relative_filename":"sygnal/http.py"}},"sygnal_notifications_py":{"hash":"e1a541d827e83bf52d9caa6916405e2c","index":{"nums":[1,97,0,9,0,0,0],"html_filename":"sygnal_notifications_py.html","relative_filename":"sygnal/notifications.py"}},"sygnal_sygnal_py":{"hash":"ad7f7a8242d2367e9a313f2f5e57f96d","index":{"nums":[1,167,0,82,0,0,0],"html_filename":"sygnal_sygnal_py.html","relative_filename":"sygnal/sygnal.py"}},"sygnal_utils_py":{"hash":"ad8cbb2bd3336e777135076fa5eab5fc","index":{"nums":[1,23,0,9,0,0,0],"html_filename":"sygnal_utils_py.html","relative_filename":"sygnal/utils.py"}},"sygnal_webpushpushkin_py":{"hash":"c4931e9cc0dc513847206da7bb25578b","index":{"nums":[1,164,0,164,0,0,0],"html_filename":"sygnal_webpushpushkin_py.html","relative_filename":"sygnal/webpushpushkin.py"}}}} \ No newline at end of file diff --git a/htmlcov/style.css b/htmlcov/style.css deleted file mode 100644 index 36ee2a6e..00000000 --- a/htmlcov/style.css +++ /dev/null @@ -1,291 +0,0 @@ -@charset "UTF-8"; -/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ -/* Don't edit this .css file. Edit the .scss file instead! */ -html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } - -body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } - -@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } - -@media (prefers-color-scheme: dark) { body { color: #eee; } } - -html > body { font-size: 16px; } - -a:active, a:focus { outline: 2px dashed #007acc; } - -p { font-size: .875em; line-height: 1.4em; } - -table { border-collapse: collapse; } - -td { vertical-align: top; } - -table tr.hidden { display: none !important; } - -p#no_rows { display: none; font-size: 1.2em; } - -a.nav { text-decoration: none; color: inherit; } - -a.nav:hover { text-decoration: underline; color: inherit; } - -#header { background: #f8f8f8; width: 100%; border-bottom: 1px solid #eee; } - -@media (prefers-color-scheme: dark) { #header { background: black; } } - -@media (prefers-color-scheme: dark) { #header { border-color: #333; } } - -.indexfile #footer { margin: 1rem 3.5rem; } - -.pyfile #footer { margin: 1rem 1rem; } - -#footer .content { padding: 0; color: #666; font-style: italic; } - -@media (prefers-color-scheme: dark) { #footer .content { color: #aaa; } } - -#index { margin: 1rem 0 0 3.5rem; } - -#header .content { padding: 1rem 3.5rem; } - -h1 { font-size: 1.25em; display: inline-block; } - -#filter_container { float: right; margin: 0 2em 0 0; } - -#filter_container input { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } - -@media (prefers-color-scheme: dark) { #filter_container input { border-color: #444; } } - -@media (prefers-color-scheme: dark) { #filter_container input { background: #1e1e1e; } } - -@media (prefers-color-scheme: dark) { #filter_container input { color: #eee; } } - -#filter_container input:focus { border-color: #007acc; } - -h2.stats { margin-top: .5em; font-size: 1em; } - -.stats button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; color: inherit; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } - -@media (prefers-color-scheme: dark) { .stats button { border-color: #444; } } - -.stats button:active, .stats button:focus { outline: 2px dashed #007acc; } - -.stats button:active, .stats button:focus { outline: 2px dashed #007acc; } - -.stats button.run { background: #eeffee; } - -@media (prefers-color-scheme: dark) { .stats button.run { background: #373d29; } } - -.stats button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { .stats button.run.show_run { background: #373d29; } } - -.stats button.mis { background: #ffeeee; } - -@media (prefers-color-scheme: dark) { .stats button.mis { background: #4b1818; } } - -.stats button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { .stats button.mis.show_mis { background: #4b1818; } } - -.stats button.exc { background: #f7f7f7; } - -@media (prefers-color-scheme: dark) { .stats button.exc { background: #333; } } - -.stats button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { .stats button.exc.show_exc { background: #333; } } - -.stats button.par { background: #ffffd5; } - -@media (prefers-color-scheme: dark) { .stats button.par { background: #650; } } - -.stats button.par.show_par { background: #ffa; border: 2px solid #dddd00; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { .stats button.par.show_par { background: #650; } } - -.help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } - -#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } - -#keyboard_icon { float: right; margin: 5px; cursor: pointer; } - -.help_panel { padding: .5em; border: 1px solid #883; } - -.help_panel .legend { font-style: italic; margin-bottom: 1em; } - -.indexfile .help_panel { width: 20em; min-height: 4em; } - -.pyfile .help_panel { width: 16em; min-height: 8em; } - -#panel_icon { float: right; cursor: pointer; } - -.keyhelp { margin: .75em; } - -.keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; } - -#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } - -#source p { position: relative; white-space: pre; } - -#source p * { box-sizing: border-box; } - -#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; } - -@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } - -#source p .n a { text-decoration: none; color: #999; } - -@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } - -#source p .n a:hover { text-decoration: underline; color: #999; } - -@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } - -#source p.highlight .n { background: #ffdd00; } - -#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } - -@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } - -#source p .t:hover { background: #f2f2f2; } - -@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } - -#source p .t:hover ~ .r .annotate.long { display: block; } - -#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } - -@media (prefers-color-scheme: dark) { #source p .t .com { color: #6A9955; } } - -#source p .t .key { font-weight: bold; line-height: 1px; } - -#source p .t .str { color: #0451A5; } - -@media (prefers-color-scheme: dark) { #source p .t .str { color: #9CDCFE; } } - -#source p.mis .t { border-left: 0.2em solid #ff0000; } - -#source p.mis.show_mis .t { background: #fdd; } - -@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } - -#source p.mis.show_mis .t:hover { background: #f2d2d2; } - -@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } - -#source p.run .t { border-left: 0.2em solid #00dd00; } - -#source p.run.show_run .t { background: #dfd; } - -@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } - -#source p.run.show_run .t:hover { background: #d2f2d2; } - -@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } - -#source p.exc .t { border-left: 0.2em solid #808080; } - -#source p.exc.show_exc .t { background: #eee; } - -@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } - -#source p.exc.show_exc .t:hover { background: #e2e2e2; } - -@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } - -#source p.par .t { border-left: 0.2em solid #dddd00; } - -#source p.par.show_par .t { background: #ffa; } - -@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } - -#source p.par.show_par .t:hover { background: #f2f2a2; } - -@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } - -#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } - -#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } - -@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } - -#source p .annotate.short:hover ~ .long { display: block; } - -#source p .annotate.long { width: 30em; right: 2.5em; } - -#source p input { display: none; } - -#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } - -#source p input ~ .r label.ctx::before { content: "▶ "; } - -#source p input ~ .r label.ctx:hover { background: #d5f7ff; color: #666; } - -@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } - -@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } - -#source p input:checked ~ .r label.ctx { background: #aef; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } - -@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } - -@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } - -#source p input:checked ~ .r label.ctx::before { content: "▼ "; } - -#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } - -#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } - -@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } - -#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #aef; border-radius: .25em; margin-right: 1.75em; } - -@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } - -#source p .ctxs span { display: block; text-align: right; } - -#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } - -#index table.index { margin-left: -.5em; } - -#index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } - -@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } - -#index td.name, #index th.name { text-align: left; width: auto; } - -#index th { font-style: italic; color: #333; cursor: pointer; } - -@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } - -#index th:hover { background: #eee; } - -@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } - -#index th.headerSortDown, #index th.headerSortUp { white-space: nowrap; background: #eee; } - -@media (prefers-color-scheme: dark) { #index th.headerSortDown, #index th.headerSortUp { background: #333; } } - -#index th.headerSortDown:after { content: " ↑"; } - -#index th.headerSortUp:after { content: " ↓"; } - -#index td.name a { text-decoration: none; color: inherit; } - -#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } - -#index tr.file:hover { background: #eee; } - -@media (prefers-color-scheme: dark) { #index tr.file:hover { background: #333; } } - -#index tr.file:hover td.name { text-decoration: underline; color: inherit; } - -#scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } - -@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } - -@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } - -#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } - -@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/htmlcov/sygnal___init___py.html b/htmlcov/sygnal___init___py.html deleted file mode 100644 index b712d435..00000000 --- a/htmlcov/sygnal___init___py.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - Coverage for sygnal/__init__.py: 100% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2020 The Matrix.org Foundation C.I.C. 

-

3# 

-

4# Licensed under the Apache License, Version 2.0 (the "License"); 

-

5# you may not use this file except in compliance with the License. 

-

6# You may obtain a copy of the License at 

-

7# 

-

8# http://www.apache.org/licenses/LICENSE-2.0 

-

9# 

-

10# Unless required by applicable law or agreed to in writing, software 

-

11# distributed under the License is distributed on an "AS IS" BASIS, 

-

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

13# See the License for the specific language governing permissions and 

-

14# limitations under the License. 

-

15 

-

16from importlib_metadata import PackageNotFoundError, version 

-

17 

-

18try: 

-

19 __version__ = version(__name__) 

-

20except PackageNotFoundError: 

-

21 # package is not installed 

-

22 pass 

-
- - - diff --git a/htmlcov/sygnal_apnspushkin_py.html b/htmlcov/sygnal_apnspushkin_py.html deleted file mode 100644 index 5a32c2f9..00000000 --- a/htmlcov/sygnal_apnspushkin_py.html +++ /dev/null @@ -1,542 +0,0 @@ - - - - - - Coverage for sygnal/apnspushkin.py: 68% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2014 OpenMarket Ltd 

-

3# Copyright 2017 Vector Creations Ltd 

-

4# Copyright 2019-2020 The Matrix.org Foundation C.I.C. 

-

5# 

-

6# Licensed under the Apache License, Version 2.0 (the "License"); 

-

7# you may not use this file except in compliance with the License. 

-

8# You may obtain a copy of the License at 

-

9# 

-

10# http://www.apache.org/licenses/LICENSE-2.0 

-

11# 

-

12# Unless required by applicable law or agreed to in writing, software 

-

13# distributed under the License is distributed on an "AS IS" BASIS, 

-

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

15# See the License for the specific language governing permissions and 

-

16# limitations under the License. 

-

17import asyncio 

-

18import base64 

-

19import copy 

-

20import logging 

-

21import os 

-

22from datetime import timezone 

-

23from typing import Dict 

-

24from uuid import uuid4 

-

25 

-

26import aioapns 

-

27from aioapns import APNs, NotificationRequest 

-

28from cryptography.hazmat.backends import default_backend 

-

29from cryptography.x509 import load_pem_x509_certificate 

-

30from opentracing import logs, tags 

-

31from prometheus_client import Counter, Gauge, Histogram 

-

32from twisted.internet.defer import Deferred 

-

33 

-

34from sygnal import apnstruncate 

-

35from sygnal.exceptions import ( 

-

36 NotificationDispatchException, 

-

37 PushkinSetupException, 

-

38 TemporaryNotificationDispatchException, 

-

39) 

-

40from sygnal.helper.proxy.proxy_asyncio import ProxyingEventLoopWrapper 

-

41from sygnal.notifications import ConcurrencyLimitedPushkin 

-

42from sygnal.utils import NotificationLoggerAdapter, twisted_sleep 

-

43 

-

44logger = logging.getLogger(__name__) 

-

45 

-

46SEND_TIME_HISTOGRAM = Histogram( 

-

47 "sygnal_apns_request_time", "Time taken to send HTTP request to APNS" 

-

48) 

-

49 

-

50ACTIVE_REQUESTS_GAUGE = Gauge( 

-

51 "sygnal_active_apns_requests", "Number of APNS requests in flight" 

-

52) 

-

53 

-

54RESPONSE_STATUS_CODES_COUNTER = Counter( 

-

55 "sygnal_apns_status_codes", 

-

56 "Number of HTTP response status codes received from APNS", 

-

57 labelnames=["pushkin", "code"], 

-

58) 

-

59 

-

60CERTIFICATE_EXPIRATION_GAUGE = Gauge( 

-

61 "sygnal_client_cert_expiry", 

-

62 "The expiry date of the client certificate in seconds since the epoch", 

-

63 labelnames=["pushkin"], 

-

64) 

-

65 

-

66 

-

67class ApnsPushkin(ConcurrencyLimitedPushkin): 

-

68 """ 

-

69 Relays notifications to the Apple Push Notification Service. 

-

70 """ 

-

71 

-

72 # Errors for which the token should be rejected 

-

73 TOKEN_ERROR_REASON = "Unregistered" 

-

74 TOKEN_ERROR_CODE = 410 

-

75 

-

76 MAX_TRIES = 3 

-

77 RETRY_DELAY_BASE = 10 

-

78 

-

79 MAX_FIELD_LENGTH = 1024 

-

80 MAX_JSON_BODY_SIZE = 4096 

-

81 

-

82 UNDERSTOOD_CONFIG_FIELDS = { 

-

83 "type", 

-

84 "platform", 

-

85 "certfile", 

-

86 "team_id", 

-

87 "key_id", 

-

88 "keyfile", 

-

89 "topic", 

-

90 } | ConcurrencyLimitedPushkin.UNDERSTOOD_CONFIG_FIELDS 

-

91 

-

92 def __init__(self, name, sygnal, config): 

-

93 super().__init__(name, sygnal, config) 

-

94 

-

95 nonunderstood = set(self.cfg.keys()).difference(self.UNDERSTOOD_CONFIG_FIELDS) 

-

96 if len(nonunderstood) > 0: 

-

97 logger.warning( 

-

98 "The following configuration fields are not understood: %s", 

-

99 nonunderstood, 

-

100 ) 

-

101 

-

102 platform = self.get_config("platform") 

-

103 if not platform or platform == "production" or platform == "prod": 

-

104 self.use_sandbox = False 

-

105 elif platform == "sandbox": 

-

106 self.use_sandbox = True 

-

107 else: 

-

108 raise PushkinSetupException(f"Invalid platform: {platform}") 

-

109 

-

110 certfile = self.get_config("certfile") 

-

111 keyfile = self.get_config("keyfile") 

-

112 if not certfile and not keyfile: 

-

113 raise PushkinSetupException( 

-

114 "You must provide a path to an APNs certificate, or an APNs token." 

-

115 ) 

-

116 

-

117 if certfile: 

-

118 if not os.path.exists(certfile): 

-

119 raise PushkinSetupException( 

-

120 f"The APNs certificate '{certfile}' does not exist." 

-

121 ) 

-

122 else: 

-

123 # keyfile 

-

124 if not os.path.exists(keyfile): 

-

125 raise PushkinSetupException( 

-

126 f"The APNs key file '{keyfile}' does not exist." 

-

127 ) 

-

128 if not self.get_config("key_id"): 

-

129 raise PushkinSetupException("You must supply key_id.") 

-

130 if not self.get_config("team_id"): 

-

131 raise PushkinSetupException("You must supply team_id.") 

-

132 if not self.get_config("topic"): 

-

133 raise PushkinSetupException("You must supply topic.") 

-

134 

-

135 # use the Sygnal global proxy configuration 

-

136 proxy_url_str = sygnal.config.get("proxy") 

-

137 

-

138 loop = asyncio.get_event_loop() 

-

139 if proxy_url_str: 

-

140 # this overrides the create_connection method to use a HTTP proxy 

-

141 loop = ProxyingEventLoopWrapper(loop, proxy_url_str) # type: ignore 

-

142 

-

143 if certfile is not None: 

-

144 # max_connection_attempts is actually the maximum number of 

-

145 # additional connection attempts, so =0 means try once only 

-

146 # (we will retry at a higher level so not worth doing more here) 

-

147 self.apns_client = APNs( 

-

148 client_cert=certfile, 

-

149 use_sandbox=self.use_sandbox, 

-

150 max_connection_attempts=0, 

-

151 loop=loop, 

-

152 ) 

-

153 

-

154 self._report_certificate_expiration(certfile) 

-

155 else: 

-

156 # max_connection_attempts is actually the maximum number of 

-

157 # additional connection attempts, so =0 means try once only 

-

158 # (we will retry at a higher level so not worth doing more here) 

-

159 self.apns_client = APNs( 

-

160 key=self.get_config("keyfile"), 

-

161 key_id=self.get_config("key_id"), 

-

162 team_id=self.get_config("team_id"), 

-

163 topic=self.get_config("topic"), 

-

164 use_sandbox=self.use_sandbox, 

-

165 max_connection_attempts=0, 

-

166 loop=loop, 

-

167 ) 

-

168 

-

169 # without this, aioapns will retry every second forever. 

-

170 self.apns_client.pool.max_connection_attempts = 3 

-

171 

-

172 def _report_certificate_expiration(self, certfile): 

-

173 """Export the epoch time that the certificate expires as a metric.""" 

-

174 with open(certfile, "rb") as f: 

-

175 cert_bytes = f.read() 

-

176 

-

177 cert = load_pem_x509_certificate(cert_bytes, default_backend()) 

-

178 # Report the expiration time as seconds since the epoch (in UTC time). 

-

179 CERTIFICATE_EXPIRATION_GAUGE.labels(pushkin=self.name).set( 

-

180 cert.not_valid_after.replace(tzinfo=timezone.utc).timestamp() 

-

181 ) 

-

182 

-

183 async def _dispatch_request(self, log, span, device, shaved_payload, prio): 

-

184 """ 

-

185 Actually attempts to dispatch the notification once. 

-

186 """ 

-

187 

-

188 # this is no good: APNs expects ID to be in their format 

-

189 # so we can't just derive a 

-

190 # notif_id = context.request_id + f"-{n.devices.index(device)}" 

-

191 

-

192 notif_id = str(uuid4()) 

-

193 

-

194 log.info(f"Sending as APNs-ID {notif_id}") 

-

195 span.set_tag("apns_id", notif_id) 

-

196 

-

197 device_token = base64.b64decode(device.pushkey).hex() 

-

198 

-

199 request = NotificationRequest( 

-

200 device_token=device_token, 

-

201 message=shaved_payload, 

-

202 priority=prio, 

-

203 notification_id=notif_id, 

-

204 ) 

-

205 

-

206 try: 

-

207 

-

208 with ACTIVE_REQUESTS_GAUGE.track_inprogress(): 

-

209 with SEND_TIME_HISTOGRAM.time(): 

-

210 response = await self._send_notification(request) 

-

211 except aioapns.ConnectionError: 

-

212 raise TemporaryNotificationDispatchException("aioapns Connection Failure") 

-

213 

-

214 code = int(response.status) 

-

215 

-

216 span.set_tag(tags.HTTP_STATUS_CODE, code) 

-

217 

-

218 RESPONSE_STATUS_CODES_COUNTER.labels(pushkin=self.name, code=code).inc() 

-

219 

-

220 if response.is_successful: 

-

221 return [] 

-

222 else: 

-

223 # .description corresponds to the 'reason' response field 

-

224 span.set_tag("apns_reason", response.description) 

-

225 if ( 

-

226 code == self.TOKEN_ERROR_CODE 

-

227 or response.description == self.TOKEN_ERROR_REASON 

-

228 ): 

-

229 return [device.pushkey] 

-

230 else: 

-

231 if 500 <= code < 600: 

-

232 raise TemporaryNotificationDispatchException( 

-

233 f"{response.status} {response.description}" 

-

234 ) 

-

235 else: 

-

236 raise NotificationDispatchException( 

-

237 f"{response.status} {response.description}" 

-

238 ) 

-

239 

-

240 async def _dispatch_notification_unlimited(self, n, device, context): 

-

241 log = NotificationLoggerAdapter(logger, {"request_id": context.request_id}) 

-

242 

-

243 # The pushkey is kind of secret because you can use it to send push 

-

244 # to someone. 

-

245 # span_tags = {"pushkey": device.pushkey} 

-

246 span_tags: Dict[str, int] = {} 

-

247 

-

248 with self.sygnal.tracer.start_span( 

-

249 "apns_dispatch", tags=span_tags, child_of=context.opentracing_span 

-

250 ) as span_parent: 

-

251 

-

252 if n.event_id and not n.type: 

-

253 payload = self._get_payload_event_id_only(n, device) 

-

254 else: 

-

255 payload = self._get_payload_full(n, device, log) 

-

256 

-

257 if payload is None: 

-

258 # Nothing to do 

-

259 span_parent.log_kv({logs.EVENT: "apns_no_payload"}) 

-

260 return 

-

261 prio = 10 

-

262 if n.prio == "low": 

-

263 prio = 5 

-

264 

-

265 shaved_payload = apnstruncate.truncate( 

-

266 payload, max_length=self.MAX_JSON_BODY_SIZE 

-

267 ) 

-

268 

-

269 for retry_number in range(self.MAX_TRIES): 

-

270 try: 

-

271 span_tags = {"retry_num": retry_number} 

-

272 

-

273 with self.sygnal.tracer.start_span( 

-

274 "apns_dispatch_try", tags=span_tags, child_of=span_parent 

-

275 ) as span: 

-

276 return await self._dispatch_request( 

-

277 log, span, device, shaved_payload, prio 

-

278 ) 

-

279 except TemporaryNotificationDispatchException as exc: 

-

280 retry_delay = self.RETRY_DELAY_BASE * (2 ** retry_number) 

-

281 if exc.custom_retry_delay is not None: 

-

282 retry_delay = exc.custom_retry_delay 

-

283 

-

284 log.warning( 

-

285 "Temporary failure, will retry in %d seconds", 

-

286 retry_delay, 

-

287 exc_info=True, 

-

288 ) 

-

289 

-

290 span_parent.log_kv( 

-

291 {"event": "temporary_fail", "retrying_in": retry_delay} 

-

292 ) 

-

293 

-

294 if retry_number == self.MAX_TRIES - 1: 

-

295 raise NotificationDispatchException( 

-

296 "Retried too many times." 

-

297 ) from exc 

-

298 else: 

-

299 await twisted_sleep( 

-

300 retry_delay, twisted_reactor=self.sygnal.reactor 

-

301 ) 

-

302 

-

303 def _get_payload_event_id_only(self, n, device): 

-

304 """ 

-

305 Constructs a payload for a notification where we know only the event ID. 

-

306 Args: 

-

307 n: The notification to construct a payload for. 

-

308 device (Device): Device information to which the constructed payload 

-

309 will be sent. 

-

310 

-

311 Returns: 

-

312 The APNs payload as a nested dicts. 

-

313 """ 

-

314 payload = {} 

-

315 

-

316 if device.data: 

-

317 payload.update(device.data.get("default_payload", {})) 

-

318 

-

319 if n.room_id: 

-

320 payload["room_id"] = n.room_id 

-

321 if n.event_id: 

-

322 payload["event_id"] = n.event_id 

-

323 

-

324 if n.counts.unread is not None: 

-

325 payload["unread_count"] = n.counts.unread 

-

326 if n.counts.missed_calls is not None: 

-

327 payload["missed_calls"] = n.counts.missed_calls 

-

328 

-

329 return payload 

-

330 

-

331 def _get_payload_full(self, n, device, log): 

-

332 """ 

-

333 Constructs a payload for a notification. 

-

334 Args: 

-

335 n: The notification to construct a payload for. 

-

336 device (Device): Device information to which the constructed payload 

-

337 will be sent. 

-

338 log: A logger. 

-

339 

-

340 Returns: 

-

341 The APNs payload as nested dicts. 

-

342 """ 

-

343 from_display = n.sender 

-

344 if n.sender_display_name is not None: 

-

345 from_display = n.sender_display_name 

-

346 from_display = from_display[0 : self.MAX_FIELD_LENGTH] 

-

347 

-

348 loc_key = None 

-

349 loc_args = None 

-

350 if n.type == "m.room.message" or n.type == "m.room.encrypted": 

-

351 room_display = None 

-

352 if n.room_name: 

-

353 room_display = n.room_name[0 : self.MAX_FIELD_LENGTH] 

-

354 elif n.room_alias: 

-

355 room_display = n.room_alias[0 : self.MAX_FIELD_LENGTH] 

-

356 

-

357 content_display = None 

-

358 action_display = None 

-

359 is_image = False 

-

360 if n.content and "msgtype" in n.content and "body" in n.content: 

-

361 if "body" in n.content: 

-

362 if n.content["msgtype"] == "m.text": 

-

363 content_display = n.content["body"] 

-

364 elif n.content["msgtype"] == "m.emote": 

-

365 action_display = n.content["body"] 

-

366 else: 

-

367 # fallback: 'body' should always be user-visible text 

-

368 # in an m.room.message 

-

369 content_display = n.content["body"] 

-

370 if n.content["msgtype"] == "m.image": 

-

371 is_image = True 

-

372 

-

373 if room_display: 

-

374 if is_image: 

-

375 loc_key = "IMAGE_FROM_USER_IN_ROOM" 

-

376 loc_args = [from_display, content_display, room_display] 

-

377 elif content_display: 

-

378 loc_key = "MSG_FROM_USER_IN_ROOM_WITH_CONTENT" 

-

379 loc_args = [from_display, room_display, content_display] 

-

380 elif action_display: 

-

381 loc_key = "ACTION_FROM_USER_IN_ROOM" 

-

382 loc_args = [room_display, from_display, action_display] 

-

383 else: 

-

384 loc_key = "MSG_FROM_USER_IN_ROOM" 

-

385 loc_args = [from_display, room_display] 

-

386 else: 

-

387 if is_image: 

-

388 loc_key = "IMAGE_FROM_USER" 

-

389 loc_args = [from_display, content_display] 

-

390 elif content_display: 

-

391 loc_key = "MSG_FROM_USER_WITH_CONTENT" 

-

392 loc_args = [from_display, content_display] 

-

393 elif action_display: 

-

394 loc_key = "ACTION_FROM_USER" 

-

395 loc_args = [from_display, action_display] 

-

396 else: 

-

397 loc_key = "MSG_FROM_USER" 

-

398 loc_args = [from_display] 

-

399 

-

400 elif n.type == "m.call.invite": 

-

401 is_video_call = False 

-

402 

-

403 # This detection works only for hs that uses WebRTC for calls 

-

404 if n.content and "offer" in n.content and "sdp" in n.content["offer"]: 

-

405 sdp = n.content["offer"]["sdp"] 

-

406 if "m=video" in sdp: 

-

407 is_video_call = True 

-

408 

-

409 if is_video_call: 

-

410 loc_key = "VIDEO_CALL_FROM_USER" 

-

411 else: 

-

412 loc_key = "VOICE_CALL_FROM_USER" 

-

413 

-

414 loc_args = [from_display] 

-

415 elif n.type == "m.room.member": 

-

416 if n.user_is_target: 

-

417 if n.membership == "invite": 

-

418 if n.room_name: 

-

419 loc_key = "USER_INVITE_TO_NAMED_ROOM" 

-

420 loc_args = [ 

-

421 from_display, 

-

422 n.room_name[0 : self.MAX_FIELD_LENGTH], 

-

423 ] 

-

424 elif n.room_alias: 

-

425 loc_key = "USER_INVITE_TO_NAMED_ROOM" 

-

426 loc_args = [ 

-

427 from_display, 

-

428 n.room_alias[0 : self.MAX_FIELD_LENGTH], 

-

429 ] 

-

430 else: 

-

431 loc_key = "USER_INVITE_TO_CHAT" 

-

432 loc_args = [from_display] 

-

433 elif n.type: 

-

434 # A type of message was received that we don't know about 

-

435 # but it was important enough for a push to have got to us 

-

436 loc_key = "MSG_FROM_USER" 

-

437 loc_args = [from_display] 

-

438 

-

439 badge = None 

-

440 if n.counts.unread is not None: 

-

441 badge = n.counts.unread 

-

442 if n.counts.missed_calls is not None: 

-

443 if badge is None: 

-

444 badge = 0 

-

445 badge += n.counts.missed_calls 

-

446 

-

447 if loc_key is None and badge is None: 

-

448 log.info("Nothing to do for alert of type %s", n.type) 

-

449 return None 

-

450 

-

451 payload = {} 

-

452 

-

453 if n.type and device.data: 

-

454 payload = copy.deepcopy(device.data.get("default_payload", {})) 

-

455 

-

456 payload.setdefault("aps", {}) 

-

457 

-

458 if loc_key: 

-

459 payload["aps"].setdefault("alert", {})["loc-key"] = loc_key 

-

460 

-

461 if loc_args: 

-

462 payload["aps"].setdefault("alert", {})["loc-args"] = loc_args 

-

463 

-

464 if badge is not None: 

-

465 payload["aps"]["badge"] = badge 

-

466 

-

467 if loc_key and n.room_id: 

-

468 payload["room_id"] = n.room_id 

-

469 if loc_key and n.event_id: 

-

470 payload["event_id"] = n.event_id 

-

471 

-

472 return payload 

-

473 

-

474 async def _send_notification(self, request): 

-

475 return await Deferred.fromFuture( 

-

476 asyncio.ensure_future(self.apns_client.send_notification(request)) 

-

477 ) 

-
- - - diff --git a/htmlcov/sygnal_apnstruncate_py.html b/htmlcov/sygnal_apnstruncate_py.html deleted file mode 100644 index 3b1100f4..00000000 --- a/htmlcov/sygnal_apnstruncate_py.html +++ /dev/null @@ -1,197 +0,0 @@ - - - - - - Coverage for sygnal/apnstruncate.py: 92% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# Copyright 2015 OpenMarket Ltd 

-

2# 

-

3# Licensed under the Apache License, Version 2.0 (the "License"); 

-

4# you may not use this file except in compliance with the License. 

-

5# You may obtain a copy of the License at 

-

6# 

-

7# http://www.apache.org/licenses/LICENSE-2.0 

-

8# 

-

9# Unless required by applicable law or agreed to in writing, software 

-

10# distributed under the License is distributed on an "AS IS" BASIS, 

-

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

12# See the License for the specific language governing permissions and 

-

13# limitations under the License. 

-

14 

-

15# Copied and adapted from 

-

16# https://raw.githubusercontent.com/matrix-org/pushbaby/master/pushbaby/truncate.py 

-

17import json 

-

18from typing import List, Tuple, Union 

-

19 

-

20 

-

21def json_encode(payload): 

-

22 return json.dumps(payload, ensure_ascii=False).encode() 

-

23 

-

24 

-

25class BodyTooLongException(Exception): 

-

26 pass 

-

27 

-

28 

-

29def is_too_long(payload, max_length=2048): 

-

30 """ 

-

31 Returns True if the given payload dictionary is too long for a push. 

-

32 Note that the maximum is now 2kB "In iOS 8 and later" although in 

-

33 practice, payloads over 256 bytes (the old limit) are still 

-

34 delivered to iOS 7 or earlier devices. 

-

35 

-

36 Maximum is 4 kiB in the new APNs with the HTTP/2 interface. 

-

37 """ 

-

38 return len(json_encode(payload)) > max_length 

-

39 

-

40 

-

41def truncate(payload, max_length=2048): 

-

42 """ 

-

43 Truncate APNs fields to make the payload fit within the max length 

-

44 specified. 

-

45 Only truncates fields that are safe to do so. 

-

46 

-

47 Args: 

-

48 payload: nested dict that will be passed to APNs 

-

49 max_length: Maximum length, in bytes, that the payload should occupy 

-

50 when JSON-encoded. 

-

51 

-

52 Returns: 

-

53 Nested dict which should comply with the maximum length restriction. 

-

54 

-

55 """ 

-

56 payload = payload.copy() 

-

57 if "aps" not in payload: 

-

58 if is_too_long(payload, max_length): 

-

59 raise BodyTooLongException() 

-

60 else: 

-

61 return payload 

-

62 aps = payload["aps"] 

-

63 

-

64 # first ensure all our choppables are str objects. 

-

65 # We need them to be for truncating to work and this 

-

66 # makes more sense than checking every time. 

-

67 for c in _choppables_for_aps(aps): 

-

68 val = _choppable_get(aps, c) 

-

69 if isinstance(val, bytes): 

-

70 _choppable_put(aps, c, val.decode()) 

-

71 

-

72 # chop off whole unicode characters until it fits (or we run out of chars) 

-

73 while is_too_long(payload, max_length): 

-

74 longest = _longest_choppable(aps) 

-

75 if longest is None: 

-

76 raise BodyTooLongException() 

-

77 

-

78 txt = _choppable_get(aps, longest) 

-

79 # Note that python's support for this is actually broken on some OSes 

-

80 # (see test_apnstruncate.py) 

-

81 txt = txt[:-1] 

-

82 _choppable_put(aps, longest, txt) 

-

83 payload["aps"] = aps 

-

84 

-

85 return payload 

-

86 

-

87 

-

88def _choppables_for_aps(aps): 

-

89 ret: List[Union[Tuple[str], Tuple[str, int]]] = [] 

-

90 if "alert" not in aps: 

-

91 return ret 

-

92 

-

93 alert = aps["alert"] 

-

94 if isinstance(alert, str): 

-

95 ret.append(("alert",)) 

-

96 elif isinstance(alert, dict): 

-

97 if "body" in alert: 

-

98 ret.append(("alert.body",)) 

-

99 if "loc-args" in alert: 

-

100 ret.extend([("alert.loc-args", i) for i in range(len(alert["loc-args"]))]) 

-

101 

-

102 return ret 

-

103 

-

104 

-

105def _choppable_get(aps, choppable): 

-

106 if choppable[0] == "alert": 

-

107 return aps["alert"] 

-

108 elif choppable[0] == "alert.body": 

-

109 return aps["alert"]["body"] 

-

110 elif choppable[0] == "alert.loc-args": 

-

111 return aps["alert"]["loc-args"][choppable[1]] 

-

112 

-

113 

-

114def _choppable_put(aps, choppable, val): 

-

115 if choppable[0] == "alert": 

-

116 aps["alert"] = val 

-

117 elif choppable[0] == "alert.body": 

-

118 aps["alert"]["body"] = val 

-

119 elif choppable[0] == "alert.loc-args": 

-

120 aps["alert"]["loc-args"][choppable[1]] = val 

-

121 

-

122 

-

123def _longest_choppable(aps): 

-

124 longest = None 

-

125 length_of_longest = 0 

-

126 for c in _choppables_for_aps(aps): 

-

127 val = _choppable_get(aps, c) 

-

128 val_len = len(val.encode()) 

-

129 if val_len > length_of_longest: 

-

130 longest = c 

-

131 length_of_longest = val_len 

-

132 return longest 

-
- - - diff --git a/htmlcov/sygnal_exceptions_py.html b/htmlcov/sygnal_exceptions_py.html deleted file mode 100644 index 29b98b03..00000000 --- a/htmlcov/sygnal_exceptions_py.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - Coverage for sygnal/exceptions.py: 100% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# Copyright 2015 OpenMarket Ltd 

-

2# 

-

3# Licensed under the Apache License, Version 2.0 (the "License"); 

-

4# you may not use this file except in compliance with the License. 

-

5# You may obtain a copy of the License at 

-

6# 

-

7# http://www.apache.org/licenses/LICENSE-2.0 

-

8# 

-

9# Unless required by applicable law or agreed to in writing, software 

-

10# distributed under the License is distributed on an "AS IS" BASIS, 

-

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

12# See the License for the specific language governing permissions and 

-

13# limitations under the License. 

-

14from twisted.internet.error import ConnectError 

-

15 

-

16 

-

17class InvalidNotificationException(Exception): 

-

18 pass 

-

19 

-

20 

-

21class PushkinSetupException(Exception): 

-

22 pass 

-

23 

-

24 

-

25class NotificationDispatchException(Exception): 

-

26 pass 

-

27 

-

28 

-

29class TemporaryNotificationDispatchException(Exception): 

-

30 """ 

-

31 To be used by pushkins for errors that are not our fault and are 

-

32 hopefully temporary, so the request should possibly be retried soon. 

-

33 """ 

-

34 

-

35 def __init__(self, *args: object, custom_retry_delay=None) -> None: 

-

36 super().__init__(*args) 

-

37 self.custom_retry_delay = custom_retry_delay 

-

38 

-

39 

-

40class ProxyConnectError(ConnectError): 

-

41 """ 

-

42 Exception raised when we are unable to start a connection using a HTTP proxy 

-

43 This indicates an issue with the HTTP Proxy in use rather than the final 

-

44 endpoint we wanted to contact. 

-

45 """ 

-

46 

-

47 pass 

-
- - - diff --git a/htmlcov/sygnal_gcmpushkin_py.html b/htmlcov/sygnal_gcmpushkin_py.html deleted file mode 100644 index 978a3c76..00000000 --- a/htmlcov/sygnal_gcmpushkin_py.html +++ /dev/null @@ -1,602 +0,0 @@ - - - - - - Coverage for sygnal/gcmpushkin.py: 75% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2014 Leon Handreke 

-

3# Copyright 2017 New Vector Ltd 

-

4# Copyright 2019-2020 The Matrix.org Foundation C.I.C. 

-

5# 

-

6# Licensed under the Apache License, Version 2.0 (the "License"); 

-

7# you may not use this file except in compliance with the License. 

-

8# You may obtain a copy of the License at 

-

9# 

-

10# http://www.apache.org/licenses/LICENSE-2.0 

-

11# 

-

12# Unless required by applicable law or agreed to in writing, software 

-

13# distributed under the License is distributed on an "AS IS" BASIS, 

-

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

15# See the License for the specific language governing permissions and 

-

16# limitations under the License. 

-

17import json 

-

18import logging 

-

19import time 

-

20from io import BytesIO 

-

21 

-

22from opentracing import logs, tags 

-

23from prometheus_client import Counter, Gauge, Histogram 

-

24from twisted.enterprise.adbapi import ConnectionPool 

-

25from twisted.internet.defer import DeferredSemaphore 

-

26from twisted.web.client import FileBodyProducer, HTTPConnectionPool, readBody 

-

27from twisted.web.http_headers import Headers 

-

28 

-

29from sygnal.exceptions import ( 

-

30 NotificationDispatchException, 

-

31 TemporaryNotificationDispatchException, 

-

32) 

-

33from sygnal.helper.context_factory import ClientTLSOptionsFactory 

-

34from sygnal.helper.proxy.proxyagent_twisted import ProxyAgent 

-

35from sygnal.utils import NotificationLoggerAdapter, json_decoder, twisted_sleep 

-

36 

-

37from .exceptions import PushkinSetupException 

-

38from .notifications import ConcurrencyLimitedPushkin 

-

39 

-

40QUEUE_TIME_HISTOGRAM = Histogram( 

-

41 "sygnal_gcm_queue_time", "Time taken waiting for a connection to GCM" 

-

42) 

-

43 

-

44SEND_TIME_HISTOGRAM = Histogram( 

-

45 "sygnal_gcm_request_time", "Time taken to send HTTP request to GCM" 

-

46) 

-

47 

-

48PENDING_REQUESTS_GAUGE = Gauge( 

-

49 "sygnal_pending_gcm_requests", "Number of GCM requests waiting for a connection" 

-

50) 

-

51 

-

52ACTIVE_REQUESTS_GAUGE = Gauge( 

-

53 "sygnal_active_gcm_requests", "Number of GCM requests in flight" 

-

54) 

-

55 

-

56RESPONSE_STATUS_CODES_COUNTER = Counter( 

-

57 "sygnal_gcm_status_codes", 

-

58 "Number of HTTP response status codes received from GCM", 

-

59 labelnames=["pushkin", "code"], 

-

60) 

-

61 

-

62logger = logging.getLogger(__name__) 

-

63 

-

64GCM_URL = b"https://fcm.googleapis.com/fcm/send" 

-

65MAX_TRIES = 3 

-

66RETRY_DELAY_BASE = 10 

-

67MAX_BYTES_PER_FIELD = 1024 

-

68 

-

69# The error codes that mean a registration ID will never 

-

70# succeed and we should reject it upstream. 

-

71# We include NotRegistered here too for good measure, even 

-

72# though gcm-client 'helpfully' extracts these into a separate 

-

73# list. 

-

74BAD_PUSHKEY_FAILURE_CODES = [ 

-

75 "MissingRegistration", 

-

76 "InvalidRegistration", 

-

77 "NotRegistered", 

-

78 "InvalidPackageName", 

-

79 "MismatchSenderId", 

-

80] 

-

81 

-

82# Failure codes that mean the message in question will never 

-

83# succeed, so don't retry, but the registration ID is fine 

-

84# so we should not reject it upstream. 

-

85BAD_MESSAGE_FAILURE_CODES = ["MessageTooBig", "InvalidDataKey", "InvalidTtl"] 

-

86 

-

87DEFAULT_MAX_CONNECTIONS = 20 

-

88 

-

89 

-

90class GcmPushkin(ConcurrencyLimitedPushkin): 

-

91 """ 

-

92 Pushkin that relays notifications to Google/Firebase Cloud Messaging. 

-

93 """ 

-

94 

-

95 UNDERSTOOD_CONFIG_FIELDS = { 

-

96 "type", 

-

97 "api_key", 

-

98 "fcm_options", 

-

99 "max_connections", 

-

100 } | ConcurrencyLimitedPushkin.UNDERSTOOD_CONFIG_FIELDS 

-

101 

-

102 def __init__(self, name, sygnal, config, canonical_reg_id_store): 

-

103 super(GcmPushkin, self).__init__(name, sygnal, config) 

-

104 

-

105 nonunderstood = set(self.cfg.keys()).difference(self.UNDERSTOOD_CONFIG_FIELDS) 

-

106 if len(nonunderstood) > 0: 

-

107 logger.warning( 

-

108 "The following configuration fields are not understood: %s", 

-

109 nonunderstood, 

-

110 ) 

-

111 

-

112 self.http_pool = HTTPConnectionPool(reactor=sygnal.reactor) 

-

113 self.max_connections = self.get_config( 

-

114 "max_connections", DEFAULT_MAX_CONNECTIONS 

-

115 ) 

-

116 self.connection_semaphore = DeferredSemaphore(self.max_connections) 

-

117 self.http_pool.maxPersistentPerHost = self.max_connections 

-

118 

-

119 tls_client_options_factory = ClientTLSOptionsFactory() 

-

120 

-

121 # use the Sygnal global proxy configuration 

-

122 proxy_url = sygnal.config.get("proxy") 

-

123 

-

124 self.http_agent = ProxyAgent( 

-

125 reactor=sygnal.reactor, 

-

126 pool=self.http_pool, 

-

127 contextFactory=tls_client_options_factory, 

-

128 proxy_url_str=proxy_url, 

-

129 ) 

-

130 

-

131 self.db = sygnal.database 

-

132 self.canonical_reg_id_store = canonical_reg_id_store 

-

133 

-

134 self.api_key = self.get_config("api_key") 

-

135 if not self.api_key: 

-

136 raise PushkinSetupException("No API key set in config") 

-

137 

-

138 # Use the fcm_options config dictionary as a foundation for the body; 

-

139 # this lets the Sygnal admin choose custom FCM options 

-

140 # (e.g. content_available). 

-

141 self.base_request_body: dict = self.get_config("fcm_options", {}) 

-

142 if not isinstance(self.base_request_body, dict): 

-

143 raise PushkinSetupException( 

-

144 "Config field fcm_options, if set, must be a dictionary of options" 

-

145 ) 

-

146 

-

147 @classmethod 

-

148 async def create(cls, name, sygnal, config): 

-

149 """ 

-

150 Override this if your pushkin needs to call async code in order to 

-

151 be constructed. Otherwise, it defaults to just invoking the Python-standard 

-

152 __init__ constructor. 

-

153 

-

154 Returns: 

-

155 an instance of this Pushkin 

-

156 """ 

-

157 logger.debug("About to set up CanonicalRegId Store") 

-

158 canonical_reg_id_store = CanonicalRegIdStore( 

-

159 sygnal.database, sygnal.database_engine 

-

160 ) 

-

161 await canonical_reg_id_store.setup() 

-

162 logger.debug("Finished setting up CanonicalRegId Store") 

-

163 

-

164 return cls(name, sygnal, config, canonical_reg_id_store) 

-

165 

-

166 async def _perform_http_request(self, body, headers): 

-

167 """ 

-

168 Perform an HTTP request to the FCM server with the body and headers 

-

169 specified. 

-

170 Args: 

-

171 body (nested dict): Body. Will be JSON-encoded. 

-

172 headers (Headers): HTTP Headers. 

-

173 

-

174 Returns: 

-

175 

-

176 """ 

-

177 body_producer = FileBodyProducer(BytesIO(json.dumps(body).encode())) 

-

178 

-

179 # we use the semaphore to actually limit the number of concurrent 

-

180 # requests, since the HTTPConnectionPool will actually just lead to more 

-

181 # requests being created but not pooled – it does not perform limiting. 

-

182 with QUEUE_TIME_HISTOGRAM.time(): 

-

183 with PENDING_REQUESTS_GAUGE.track_inprogress(): 

-

184 await self.connection_semaphore.acquire() 

-

185 

-

186 try: 

-

187 with SEND_TIME_HISTOGRAM.time(): 

-

188 with ACTIVE_REQUESTS_GAUGE.track_inprogress(): 

-

189 response = await self.http_agent.request( 

-

190 b"POST", 

-

191 GCM_URL, 

-

192 headers=Headers(headers), 

-

193 bodyProducer=body_producer, 

-

194 ) 

-

195 response_text = (await readBody(response)).decode() 

-

196 except Exception as exception: 

-

197 raise TemporaryNotificationDispatchException( 

-

198 "GCM request failure" 

-

199 ) from exception 

-

200 finally: 

-

201 self.connection_semaphore.release() 

-

202 return response, response_text 

-

203 

-

204 async def _request_dispatch(self, n, log, body, headers, pushkeys, span): 

-

205 poke_start_time = time.time() 

-

206 

-

207 failed = [] 

-

208 

-

209 response, response_text = await self._perform_http_request(body, headers) 

-

210 

-

211 RESPONSE_STATUS_CODES_COUNTER.labels( 

-

212 pushkin=self.name, code=response.code 

-

213 ).inc() 

-

214 

-

215 log.debug("GCM request took %f seconds", time.time() - poke_start_time) 

-

216 

-

217 span.set_tag(tags.HTTP_STATUS_CODE, response.code) 

-

218 

-

219 if 500 <= response.code < 600: 

-

220 log.debug("%d from server, waiting to try again", response.code) 

-

221 

-

222 retry_after = None 

-

223 

-

224 for header_value in response.headers.getRawHeaders( 

-

225 b"retry-after", default=[] 

-

226 ): 

-

227 retry_after = int(header_value) 

-

228 span.log_kv({"event": "gcm_retry_after", "retry_after": retry_after}) 

-

229 

-

230 raise TemporaryNotificationDispatchException( 

-

231 "GCM server error, hopefully temporary.", custom_retry_delay=retry_after 

-

232 ) 

-

233 elif response.code == 400: 

-

234 log.error( 

-

235 "%d from server, we have sent something invalid! Error: %r", 

-

236 response.code, 

-

237 response_text, 

-

238 ) 

-

239 # permanent failure: give up 

-

240 raise NotificationDispatchException("Invalid request") 

-

241 elif response.code == 401: 

-

242 log.error( 

-

243 "401 from server! Our API key is invalid? Error: %r", response_text 

-

244 ) 

-

245 # permanent failure: give up 

-

246 raise NotificationDispatchException("Not authorised to push") 

-

247 elif response.code == 404: 

-

248 # assume they're all failed 

-

249 log.info("Reg IDs %r get 404 response; assuming unregistered", pushkeys) 

-

250 return pushkeys, [] 

-

251 elif 200 <= response.code < 300: 

-

252 try: 

-

253 resp_object = json_decoder.decode(response_text) 

-

254 except ValueError: 

-

255 raise NotificationDispatchException("Invalid JSON response from GCM.") 

-

256 if "results" not in resp_object: 

-

257 log.error( 

-

258 "%d from server but response contained no 'results' key: %r", 

-

259 response.code, 

-

260 response_text, 

-

261 ) 

-

262 if len(resp_object["results"]) < len(pushkeys): 

-

263 log.error( 

-

264 "Sent %d notifications but only got %d responses!", 

-

265 len(n.devices), 

-

266 len(resp_object["results"]), 

-

267 ) 

-

268 span.log_kv( 

-

269 { 

-

270 logs.EVENT: "gcm_response_mismatch", 

-

271 "num_devices": len(n.devices), 

-

272 "num_results": len(resp_object["results"]), 

-

273 } 

-

274 ) 

-

275 

-

276 # determine which pushkeys to retry or forget about 

-

277 new_pushkeys = [] 

-

278 for i, result in enumerate(resp_object["results"]): 

-

279 span.set_tag("gcm_regid_updated", "registration_id" in result) 

-

280 if "registration_id" in result: 

-

281 await self.canonical_reg_id_store.set_canonical_id( 

-

282 pushkeys[i], result["registration_id"] 

-

283 ) 

-

284 if "error" in result: 

-

285 log.warning( 

-

286 "Error for pushkey %s: %s", pushkeys[i], result["error"] 

-

287 ) 

-

288 span.set_tag("gcm_error", result["error"]) 

-

289 if result["error"] in BAD_PUSHKEY_FAILURE_CODES: 

-

290 log.info( 

-

291 "Reg ID %r has permanently failed with code %r: " 

-

292 "rejecting upstream", 

-

293 pushkeys[i], 

-

294 result["error"], 

-

295 ) 

-

296 failed.append(pushkeys[i]) 

-

297 elif result["error"] in BAD_MESSAGE_FAILURE_CODES: 

-

298 log.info( 

-

299 "Message for reg ID %r has permanently failed with code %r", 

-

300 pushkeys[i], 

-

301 result["error"], 

-

302 ) 

-

303 else: 

-

304 log.info( 

-

305 "Reg ID %r has temporarily failed with code %r", 

-

306 pushkeys[i], 

-

307 result["error"], 

-

308 ) 

-

309 new_pushkeys.append(pushkeys[i]) 

-

310 return failed, new_pushkeys 

-

311 else: 

-

312 raise NotificationDispatchException( 

-

313 f"Unknown GCM response code {response.code}" 

-

314 ) 

-

315 

-

316 async def _dispatch_notification_unlimited(self, n, device, context): 

-

317 log = NotificationLoggerAdapter(logger, {"request_id": context.request_id}) 

-

318 

-

319 pushkeys = [ 

-

320 device.pushkey for device in n.devices if device.app_id == self.name 

-

321 ] 

-

322 # Resolve canonical IDs for all pushkeys 

-

323 

-

324 if pushkeys[0] != device.pushkey: 

-

325 # Only send notifications once, to all devices at once. 

-

326 return [] 

-

327 

-

328 # The pushkey is kind of secret because you can use it to send push 

-

329 # to someone. 

-

330 # span_tags = {"pushkeys": pushkeys} 

-

331 span_tags = {"gcm_num_devices": len(pushkeys)} 

-

332 

-

333 with self.sygnal.tracer.start_span( 

-

334 "gcm_dispatch", tags=span_tags, child_of=context.opentracing_span 

-

335 ) as span_parent: 

-

336 reg_id_mappings = await self.canonical_reg_id_store.get_canonical_ids( 

-

337 pushkeys 

-

338 ) 

-

339 

-

340 reg_id_mappings = { 

-

341 reg_id: canonical_reg_id or reg_id 

-

342 for (reg_id, canonical_reg_id) in reg_id_mappings.items() 

-

343 } 

-

344 

-

345 inverse_reg_id_mappings = {v: k for (k, v) in reg_id_mappings.items()} 

-

346 

-

347 data = GcmPushkin._build_data(n, device) 

-

348 headers = { 

-

349 b"User-Agent": ["sygnal"], 

-

350 b"Content-Type": ["application/json"], 

-

351 b"Authorization": ["key=%s" % (self.api_key,)], 

-

352 } 

-

353 

-

354 # count the number of remapped registration IDs in the request 

-

355 span_parent.set_tag( 

-

356 "gcm_num_remapped_reg_ids_used", 

-

357 [k != v for (k, v) in reg_id_mappings.items()].count(True), 

-

358 ) 

-

359 

-

360 # TODO: Implement collapse_key to queue only one message per room. 

-

361 failed = [] 

-

362 

-

363 body = self.base_request_body.copy() 

-

364 body["data"] = data 

-

365 body["priority"] = "normal" if n.prio == "low" else "high" 

-

366 

-

367 for retry_number in range(0, MAX_TRIES): 

-

368 mapped_pushkeys = [reg_id_mappings[pk] for pk in pushkeys] 

-

369 

-

370 if len(pushkeys) == 1: 

-

371 body["to"] = mapped_pushkeys[0] 

-

372 else: 

-

373 body["registration_ids"] = mapped_pushkeys 

-

374 

-

375 log.info("Sending (attempt %i) => %r", retry_number, mapped_pushkeys) 

-

376 

-

377 try: 

-

378 span_tags = {"retry_num": retry_number} 

-

379 

-

380 with self.sygnal.tracer.start_span( 

-

381 "gcm_dispatch_try", tags=span_tags, child_of=span_parent 

-

382 ) as span: 

-

383 new_failed, new_pushkeys = await self._request_dispatch( 

-

384 n, log, body, headers, mapped_pushkeys, span 

-

385 ) 

-

386 pushkeys = new_pushkeys 

-

387 failed += [ 

-

388 inverse_reg_id_mappings[canonical_pk] 

-

389 for canonical_pk in new_failed 

-

390 ] 

-

391 if len(pushkeys) == 0: 

-

392 break 

-

393 except TemporaryNotificationDispatchException as exc: 

-

394 retry_delay = RETRY_DELAY_BASE * (2 ** retry_number) 

-

395 if exc.custom_retry_delay is not None: 

-

396 retry_delay = exc.custom_retry_delay 

-

397 

-

398 log.warning( 

-

399 "Temporary failure, will retry in %d seconds", 

-

400 retry_delay, 

-

401 exc_info=True, 

-

402 ) 

-

403 

-

404 span_parent.log_kv( 

-

405 {"event": "temporary_fail", "retrying_in": retry_delay} 

-

406 ) 

-

407 

-

408 await twisted_sleep( 

-

409 retry_delay, twisted_reactor=self.sygnal.reactor 

-

410 ) 

-

411 

-

412 if len(pushkeys) > 0: 

-

413 log.info("Gave up retrying reg IDs: %r", pushkeys) 

-

414 # Count the number of failed devices. 

-

415 span_parent.set_tag("gcm_num_failed", len(failed)) 

-

416 return failed 

-

417 

-

418 @staticmethod 

-

419 def _build_data(n, device): 

-

420 """ 

-

421 Build the payload data to be sent. 

-

422 Args: 

-

423 n: Notification to build the payload for. 

-

424 device (Device): Device information to which the constructed payload 

-

425 will be sent. 

-

426 

-

427 Returns: 

-

428 JSON-compatible dict 

-

429 """ 

-

430 data = {} 

-

431 

-

432 if device.data: 

-

433 data.update(device.data.get("default_payload", {})) 

-

434 

-

435 for attr in [ 

-

436 "event_id", 

-

437 "type", 

-

438 "sender", 

-

439 "room_name", 

-

440 "room_alias", 

-

441 "membership", 

-

442 "sender_display_name", 

-

443 "content", 

-

444 "room_id", 

-

445 ]: 

-

446 if hasattr(n, attr): 

-

447 data[attr] = getattr(n, attr) 

-

448 # Truncate fields to a sensible maximum length. If the whole 

-

449 # body is too long, GCM will reject it. 

-

450 if data[attr] is not None and len(data[attr]) > MAX_BYTES_PER_FIELD: 

-

451 data[attr] = data[attr][0:MAX_BYTES_PER_FIELD] 

-

452 

-

453 data["prio"] = "high" 

-

454 if n.prio == "low": 

-

455 data["prio"] = "normal" 

-

456 

-

457 if getattr(n, "counts", None): 

-

458 data["unread"] = n.counts.unread 

-

459 data["missed_calls"] = n.counts.missed_calls 

-

460 

-

461 return data 

-

462 

-

463 

-

464class CanonicalRegIdStore(object): 

-

465 TABLE_CREATE_QUERY = """ 

-

466 CREATE TABLE IF NOT EXISTS gcm_canonical_reg_id ( 

-

467 reg_id TEXT PRIMARY KEY, 

-

468 canonical_reg_id TEXT NOT NULL 

-

469 ); 

-

470 """ 

-

471 

-

472 def __init__(self, db: ConnectionPool, engine: str): 

-

473 """ 

-

474 Args: 

-

475 db (adbapi.ConnectionPool): database to prepare 

-

476 engine (str): 

-

477 Database engine to use. Shoud be either "sqlite" or "postgresql". 

-

478 """ 

-

479 self.db = db 

-

480 self.engine = engine 

-

481 

-

482 async def setup(self): 

-

483 """ 

-

484 Prepares, if necessary, the database for storing canonical registration IDs. 

-

485 

-

486 Separate method from the constructor because we wait for an async request 

-

487 to complete, so it must be an `async def` method. 

-

488 """ 

-

489 await self.db.runOperation(self.TABLE_CREATE_QUERY) 

-

490 

-

491 async def set_canonical_id(self, reg_id, canonical_reg_id): 

-

492 """ 

-

493 Associates a GCM registration ID with a canonical registration ID. 

-

494 Args: 

-

495 reg_id (str): a registration ID 

-

496 canonical_reg_id (str): the canonical registration ID for `reg_id` 

-

497 """ 

-

498 if self.engine == "sqlite": 

-

499 await self.db.runOperation( 

-

500 "INSERT OR REPLACE INTO gcm_canonical_reg_id VALUES (?, ?);", 

-

501 (reg_id, canonical_reg_id), 

-

502 ) 

-

503 else: 

-

504 await self.db.runOperation( 

-

505 """ 

-

506 INSERT INTO gcm_canonical_reg_id VALUES (%s, %s) 

-

507 ON CONFLICT (reg_id) DO UPDATE 

-

508 SET canonical_reg_id = EXCLUDED.canonical_reg_id; 

-

509 """, 

-

510 (reg_id, canonical_reg_id), 

-

511 ) 

-

512 

-

513 async def get_canonical_ids(self, reg_ids): 

-

514 """ 

-

515 Retrieves the canonical registration ID for multiple registration IDs. 

-

516 

-

517 Args: 

-

518 reg_ids (iterable): registration IDs to retrieve canonical registration 

-

519 IDs for. 

-

520 

-

521 Returns (dict): 

-

522 mapping of registration ID to either its canonical registration ID, 

-

523 or `None` if there is no entry. 

-

524 """ 

-

525 parameter_key = "?" if self.engine == "sqlite" else "%s" 

-

526 rows = dict( 

-

527 await self.db.runQuery( 

-

528 """ 

-

529 SELECT reg_id, canonical_reg_id 

-

530 FROM gcm_canonical_reg_id 

-

531 WHERE reg_id IN (%s) 

-

532 """ 

-

533 % (",".join(parameter_key for _ in reg_ids)), 

-

534 reg_ids, 

-

535 ) 

-

536 ) 

-

537 return {reg_id: dict(rows).get(reg_id) for reg_id in reg_ids} 

-
- - - diff --git a/htmlcov/sygnal_helper___init___py.html b/htmlcov/sygnal_helper___init___py.html deleted file mode 100644 index b3bc4215..00000000 --- a/htmlcov/sygnal_helper___init___py.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - Coverage for sygnal/helper/__init__.py: 100% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-
- - - diff --git a/htmlcov/sygnal_helper_context_factory_py.html b/htmlcov/sygnal_helper_context_factory_py.html deleted file mode 100644 index c8dbf4a3..00000000 --- a/htmlcov/sygnal_helper_context_factory_py.html +++ /dev/null @@ -1,221 +0,0 @@ - - - - - - Coverage for sygnal/helper/context_factory.py: 48% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# Copyright 2014-2016 OpenMarket Ltd 

-

2# Copyright 2019 New Vector Ltd 

-

3# 

-

4# Licensed under the Apache License, Version 2.0 (the "License"); 

-

5# you may not use this file except in compliance with the License. 

-

6# You may obtain a copy of the License at 

-

7# 

-

8# http://www.apache.org/licenses/LICENSE-2.0 

-

9# 

-

10# Unless required by applicable law or agreed to in writing, software 

-

11# distributed under the License is distributed on an "AS IS" BASIS, 

-

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

13# See the License for the specific language governing permissions and 

-

14# limitations under the License. 

-

15 

-

16# Adapted from Synapse: 

-

17# https://github.com/matrix-org/synapse/blob/1016f303e58b1305ed5b3572fde002e1273e0fc0/synapse/crypto/context_factory.py#L77 

-

18 

-

19 

-

20import logging 

-

21 

-

22import idna 

-

23from OpenSSL import SSL 

-

24from service_identity import VerificationError 

-

25from service_identity.pyopenssl import verify_hostname, verify_ip_address 

-

26from twisted.internet.abstract import isIPAddress, isIPv6Address 

-

27from twisted.internet.interfaces import IOpenSSLClientConnectionCreator 

-

28from twisted.internet.ssl import CertificateOptions, TLSVersion, platformTrust 

-

29from twisted.python.failure import Failure 

-

30from twisted.web.iweb import IPolicyForHTTPS 

-

31from zope.interface import implementer 

-

32 

-

33logger = logging.getLogger(__name__) 

-

34 

-

35 

-

36@implementer(IPolicyForHTTPS) 

-

37class ClientTLSOptionsFactory(object): 

-

38 """Factory for Twisted SSLClientConnectionCreators that are used to make connections 

-

39 to remote servers for federation. 

-

40 Uses one of two OpenSSL context objects for all connections, depending on whether 

-

41 we should do SSL certificate verification. 

-

42 get_options decides whether we should do SSL certificate verification and 

-

43 constructs an SSLClientConnectionCreator factory accordingly. 

-

44 """ 

-

45 

-

46 def __init__(self): 

-

47 # Use CA root certs provided by OpenSSL 

-

48 trust_root = platformTrust() 

-

49 

-

50 # "insecurelyLowerMinimumTo" is the argument that will go lower than 

-

51 # Twisted's default, which is why it is marked as "insecure" (since 

-

52 # Twisted's defaults are reasonably secure). But, since Twisted is 

-

53 # moving to TLS 1.2 by default, we want to respect the config option if 

-

54 # it is set to 1.0 (which the alternate option, raiseMinimumTo, will not 

-

55 # let us do). 

-

56 minTLS = TLSVersion.TLSv1_2 

-

57 

-

58 self._verify_ssl = CertificateOptions( 

-

59 trustRoot=trust_root, insecurelyLowerMinimumTo=minTLS 

-

60 ) 

-

61 self._verify_ssl_context = self._verify_ssl.getContext() 

-

62 self._verify_ssl_context.set_info_callback(self._context_info_cb) 

-

63 

-

64 def get_options(self, host): 

-

65 ssl_context = self._verify_ssl_context 

-

66 

-

67 return SSLClientConnectionCreator(host, ssl_context) 

-

68 

-

69 @staticmethod 

-

70 def _context_info_cb(ssl_connection, where, ret): 

-

71 """The 'information callback' for our openssl context object.""" 

-

72 # we assume that the app_data on the connection object has been set to 

-

73 # a TLSMemoryBIOProtocol object. (This is done by SSLClientConnectionCreator) 

-

74 tls_protocol = ssl_connection.get_app_data() 

-

75 try: 

-

76 # ... we further assume that SSLClientConnectionCreator has set the 

-

77 # '_synapse_tls_verifier' attribute to a ConnectionVerifier object. 

-

78 tls_protocol._synapse_tls_verifier.verify_context_info_cb( 

-

79 ssl_connection, where 

-

80 ) 

-

81 except: # noqa: E722, taken from the twisted implementation 

-

82 logger.exception("Error during info_callback") 

-

83 f = Failure() 

-

84 tls_protocol.failVerification(f) 

-

85 

-

86 def creatorForNetloc(self, hostname, port): 

-

87 """Implements the IPolicyForHTTPS interace so that this can be passed 

-

88 directly to agents. 

-

89 """ 

-

90 return self.get_options(hostname) 

-

91 

-

92 

-

93@implementer(IOpenSSLClientConnectionCreator) 

-

94class SSLClientConnectionCreator(object): 

-

95 """Creates openssl connection objects for client connections. 

-

96 

-

97 Replaces twisted.internet.ssl.ClientTLSOptions 

-

98 """ 

-

99 

-

100 def __init__(self, hostname, ctx): 

-

101 self._ctx = ctx 

-

102 self._verifier = ConnectionVerifier(hostname) 

-

103 

-

104 def clientConnectionForTLS(self, tls_protocol): 

-

105 context = self._ctx 

-

106 connection = SSL.Connection(context, None) 

-

107 

-

108 # as per twisted.internet.ssl.ClientTLSOptions, we set the application 

-

109 # data to our TLSMemoryBIOProtocol... 

-

110 connection.set_app_data(tls_protocol) 

-

111 

-

112 # ... and we also gut-wrench a '_synapse_tls_verifier' attribute into the 

-

113 # tls_protocol so that the SSL context's info callback has something to 

-

114 # call to do the cert verification. 

-

115 setattr(tls_protocol, "_synapse_tls_verifier", self._verifier) 

-

116 return connection 

-

117 

-

118 

-

119class ConnectionVerifier(object): 

-

120 """Set the SNI, and do cert verification 

-

121 

-

122 This is a thing which is attached to the TLSMemoryBIOProtocol, and is called by 

-

123 the ssl context's info callback. 

-

124 """ 

-

125 

-

126 # This code is based on twisted.internet.ssl.ClientTLSOptions. 

-

127 

-

128 def __init__(self, hostname): 

-

129 if isIPAddress(hostname) or isIPv6Address(hostname): 

-

130 self._hostnameBytes = hostname.encode("ascii") 

-

131 self._is_ip_address = True 

-

132 else: 

-

133 # twisted's ClientTLSOptions falls back to the stdlib impl here if 

-

134 # idna is not installed, but points out that lacks support for 

-

135 # IDNA2008 (http://bugs.python.org/issue17305). 

-

136 # 

-

137 # We can rely on having idna. 

-

138 self._hostnameBytes = idna.encode(hostname) 

-

139 self._is_ip_address = False 

-

140 

-

141 self._hostnameASCII = self._hostnameBytes.decode("ascii") 

-

142 

-

143 def verify_context_info_cb(self, ssl_connection, where): 

-

144 if where & SSL.SSL_CB_HANDSHAKE_START and not self._is_ip_address: 

-

145 ssl_connection.set_tlsext_host_name(self._hostnameBytes) 

-

146 

-

147 if where & SSL.SSL_CB_HANDSHAKE_DONE: 

-

148 try: 

-

149 if self._is_ip_address: 

-

150 verify_ip_address(ssl_connection, self._hostnameASCII) 

-

151 else: 

-

152 verify_hostname(ssl_connection, self._hostnameASCII) 

-

153 except VerificationError: 

-

154 f = Failure() 

-

155 tls_protocol = ssl_connection.get_app_data() 

-

156 tls_protocol.failVerification(f) 

-
- - - diff --git a/htmlcov/sygnal_helper_proxy___init___py.html b/htmlcov/sygnal_helper_proxy___init___py.html deleted file mode 100644 index fc2c1586..00000000 --- a/htmlcov/sygnal_helper_proxy___init___py.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - Coverage for sygnal/helper/proxy/__init__.py: 100% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2020 The Matrix.org Foundation C.I.C. 

-

3# 

-

4# Licensed under the Apache License, Version 2.0 (the "License"); 

-

5# you may not use this file except in compliance with the License. 

-

6# You may obtain a copy of the License at 

-

7# 

-

8# http://www.apache.org/licenses/LICENSE-2.0 

-

9# 

-

10# Unless required by applicable law or agreed to in writing, software 

-

11# distributed under the License is distributed on an "AS IS" BASIS, 

-

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

13# See the License for the specific language governing permissions and 

-

14# limitations under the License. 

-

15from typing import NamedTuple, Optional, Tuple 

-

16from urllib.parse import urlparse 

-

17 

-

18""" 

-

19 HttpProxyUrl represents a HTTP proxy URL and no more. 

-

20 

-

21 hostname is a string with the pure hostname (or IP address). 

-

22 port is always an integer; a default port number used if necessary. 

-

23 credentials is None or a tuple of (username, password) strings. 

-

24""" 

-

25HttpProxyUrl = NamedTuple( 

-

26 "HttpProxyUrl", 

-

27 [("hostname", str), ("port", int), ("credentials", Optional[Tuple[str, str]])], 

-

28) 

-

29 

-

30 

-

31def decompose_http_proxy_url(proxy_url: str) -> HttpProxyUrl: 

-

32 """ 

-

33 Given a HTTP proxy URL, breaks it down into components and checks that it 

-

34 has a hostname (otherwise it is not right useful to us trying to find a 

-

35 proxy) and asserts that the URL has the 'http' scheme as that is all we 

-

36 support. 

-

37 

-

38 Args: 

-

39 proxy_url: 

-

40 The proxy URL, as a string. 

-

41 e.g. 'http://user:password@prox:8080' or just 'http://prox' or 

-

42 anything in between. 

-

43 

-

44 Returns: 

-

45 A `HttpProxyUrl` namedtuple with the separate information relevant for 

-

46 connecting to a proxy. 

-

47 """ 

-

48 url = urlparse(proxy_url, scheme="http") 

-

49 

-

50 if not url.hostname: 

-

51 raise RuntimeError("Proxy URL did not contain a hostname! Please specify one.") 

-

52 

-

53 if url.scheme != "http": 

-

54 raise RuntimeError( 

-

55 f"Unknown proxy scheme {url.scheme}; only 'http' is supported." 

-

56 ) 

-

57 

-

58 credentials = None 

-

59 if url.username and url.password: 

-

60 credentials = (url.username, url.password) 

-

61 

-

62 return HttpProxyUrl( 

-

63 hostname=url.hostname, port=url.port or 80, credentials=credentials 

-

64 ) 

-
- - - diff --git a/htmlcov/sygnal_helper_proxy_connectproxyclient_twisted_py.html b/htmlcov/sygnal_helper_proxy_connectproxyclient_twisted_py.html deleted file mode 100644 index 47e684f4..00000000 --- a/htmlcov/sygnal_helper_proxy_connectproxyclient_twisted_py.html +++ /dev/null @@ -1,310 +0,0 @@ - - - - - - Coverage for sygnal/helper/proxy/connectproxyclient_twisted.py: 85% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2019-2020 The Matrix.org Foundation C.I.C. 

-

3# 

-

4# Licensed under the Apache License, Version 2.0 (the "License"); 

-

5# you may not use this file except in compliance with the License. 

-

6# You may obtain a copy of the License at 

-

7# 

-

8# http://www.apache.org/licenses/LICENSE-2.0 

-

9# 

-

10# Unless required by applicable law or agreed to in writing, software 

-

11# distributed under the License is distributed on an "AS IS" BASIS, 

-

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

13# See the License for the specific language governing permissions and 

-

14# limitations under the License. 

-

15 

-

16# Adapted from Synapse: 

-

17# https://github.com/matrix-org/synapse/blob/6920e58136671f086536332bdd6844dff0d4b429/synapse/http/connectproxyclient.py 

-

18 

-

19import logging 

-

20from base64 import urlsafe_b64encode 

-

21from typing import Optional, Tuple 

-

22 

-

23from twisted.internet import defer, protocol 

-

24from twisted.internet.base import ReactorBase 

-

25from twisted.internet.defer import Deferred 

-

26from twisted.internet.interfaces import IProtocolFactory, IStreamClientEndpoint 

-

27from twisted.internet.protocol import Protocol, connectionDone 

-

28from twisted.web import http 

-

29from zope.interface import implementer 

-

30 

-

31from sygnal.exceptions import ProxyConnectError 

-

32 

-

33logger = logging.getLogger(__name__) 

-

34 

-

35 

-

36@implementer(IStreamClientEndpoint) 

-

37class HTTPConnectProxyEndpoint(object): 

-

38 """An Endpoint implementation which will send a CONNECT request to an http proxy 

-

39 

-

40 Wraps an existing HostnameEndpoint for the proxy. 

-

41 

-

42 When we get the connect() request from the connection pool (via the TLS wrapper), 

-

43 we'll first connect to the proxy endpoint with a ProtocolFactory which will make the 

-

44 CONNECT request. Once that completes, we invoke the protocolFactory which was passed 

-

45 in. 

-

46 

-

47 Args: 

-

48 reactor: the Twisted reactor to use for the connection 

-

49 proxy_endpoint (IStreamClientEndpoint): the endpoint to use to connect to the 

-

50 proxy 

-

51 host (bytes): hostname that we want to CONNECT to 

-

52 port (int): port that we want to connect to 

-

53 proxy_auth (tuple): None or tuple of (username, pasword) for HTTP basic proxy 

-

54 authentication 

-

55 """ 

-

56 

-

57 def __init__( 

-

58 self, 

-

59 reactor: ReactorBase, 

-

60 proxy_endpoint: IStreamClientEndpoint, 

-

61 host: bytes, 

-

62 port: int, 

-

63 proxy_auth: Optional[Tuple[str, str]], 

-

64 ): 

-

65 self._reactor = reactor 

-

66 self._proxy_endpoint = proxy_endpoint 

-

67 self._host = host 

-

68 self._port = port 

-

69 self._proxy_auth = proxy_auth 

-

70 

-

71 def __repr__(self): 

-

72 return "<HTTPConnectProxyEndpoint %s>" % (self._proxy_endpoint,) 

-

73 

-

74 def connect(self, protocolFactory: IProtocolFactory): 

-

75 assert isinstance(protocolFactory, protocol.ClientFactory) 

-

76 f = HTTPProxiedClientFactory( 

-

77 self._host, self._port, self._proxy_auth, protocolFactory 

-

78 ) 

-

79 d = self._proxy_endpoint.connect(f) 

-

80 # once the tcp socket connects successfully, we need to wait for the 

-

81 # CONNECT to complete. 

-

82 d.addCallback(lambda conn: f.on_connection) 

-

83 return d 

-

84 

-

85 

-

86class HTTPProxiedClientFactory(protocol.ClientFactory): 

-

87 """ClientFactory wrapper that triggers an HTTP proxy CONNECT on connect. 

-

88 

-

89 It invokes the original ClientFactory to build the HTTP Protocol object, 

-

90 and then, once CONNECT is completed, uses it to run the rest of the 

-

91 connection. 

-

92 

-

93 Args: 

-

94 dst_host: hostname that we want to CONNECT to 

-

95 dst_port: port that we want to connect to 

-

96 proxy_auth: None or tuple of (username, pasword) for HTTP basic proxy 

-

97 authentication 

-

98 wrapped_factory: The original Factory 

-

99 """ 

-

100 

-

101 def __init__( 

-

102 self, 

-

103 dst_host: bytes, 

-

104 dst_port: int, 

-

105 proxy_auth: Optional[Tuple[str, str]], 

-

106 wrapped_factory: protocol.ClientFactory, 

-

107 ): 

-

108 self.dst_host = dst_host 

-

109 self.dst_port = dst_port 

-

110 self._proxy_auth = proxy_auth 

-

111 self.wrapped_factory = wrapped_factory 

-

112 self.on_connection = defer.Deferred() 

-

113 

-

114 def startedConnecting(self, connector): 

-

115 return self.wrapped_factory.startedConnecting(connector) 

-

116 

-

117 def buildProtocol(self, addr): 

-

118 wrapped_protocol = self.wrapped_factory.buildProtocol(addr) 

-

119 

-

120 return HTTPConnectProtocol( 

-

121 self.dst_host, 

-

122 self.dst_port, 

-

123 self._proxy_auth, 

-

124 wrapped_protocol, 

-

125 self.on_connection, 

-

126 ) 

-

127 

-

128 def clientConnectionFailed(self, connector, reason): 

-

129 logger.debug("Connection to proxy failed: %s", reason) 

-

130 if not self.on_connection.called: 

-

131 self.on_connection.errback(reason) 

-

132 return self.wrapped_factory.clientConnectionFailed(connector, reason) 

-

133 

-

134 def clientConnectionLost(self, connector, reason): 

-

135 logger.debug("Connection to proxy lost: %s", reason) 

-

136 if not self.on_connection.called: 

-

137 self.on_connection.errback(reason) 

-

138 return self.wrapped_factory.clientConnectionLost(connector, reason) 

-

139 

-

140 

-

141class HTTPConnectProtocol(protocol.Protocol): 

-

142 """Protocol that wraps an existing Protocol to do a CONNECT handshake at connect 

-

143 

-

144 Args: 

-

145 host: The original HTTP(s) hostname or IPv4 or IPv6 address literal 

-

146 to put in the CONNECT request 

-

147 

-

148 port: The original HTTP(s) port to put in the CONNECT request 

-

149 

-

150 proxy_auth: None or tuple of (username, pasword) for HTTP basic proxy 

-

151 authentication 

-

152 

-

153 wrapped_protocol: the original protocol (probably 

-

154 HTTPChannel or TLSMemoryBIOProtocol, but could be anything really) 

-

155 

-

156 connected_deferred: a Deferred which will be callbacked with 

-

157 wrapped_protocol when the CONNECT completes 

-

158 """ 

-

159 

-

160 def __init__( 

-

161 self, 

-

162 host: bytes, 

-

163 port: int, 

-

164 proxy_auth: Optional[Tuple[str, str]], 

-

165 wrapped_protocol: Protocol, 

-

166 connected_deferred: Deferred, 

-

167 ): 

-

168 self.host = host 

-

169 self.port = port 

-

170 self.wrapped_protocol = wrapped_protocol 

-

171 self.connected_deferred = connected_deferred 

-

172 self.http_setup_client = HTTPConnectSetupClient( 

-

173 self.host, self.port, proxy_auth 

-

174 ) 

-

175 self.http_setup_client.on_connected.addCallback(self.proxyConnected) 

-

176 

-

177 def connectionMade(self): 

-

178 self.http_setup_client.makeConnection(self.transport) 

-

179 

-

180 def connectionLost(self, reason=connectionDone): 

-

181 if self.wrapped_protocol.connected: 

-

182 self.wrapped_protocol.connectionLost(reason) 

-

183 

-

184 self.http_setup_client.connectionLost(reason) 

-

185 

-

186 if not self.connected_deferred.called: 

-

187 self.connected_deferred.errback(reason) 

-

188 

-

189 def proxyConnected(self, _): 

-

190 self.wrapped_protocol.makeConnection(self.transport) 

-

191 

-

192 self.connected_deferred.callback(self.wrapped_protocol) 

-

193 

-

194 # Get any pending data from the http buf and forward it to the original protocol 

-

195 buf = self.http_setup_client.clearLineBuffer() 

-

196 if buf: 

-

197 self.wrapped_protocol.dataReceived(buf) 

-

198 

-

199 def dataReceived(self, data): 

-

200 # if we've set up the HTTP protocol, we can send the data there 

-

201 if self.wrapped_protocol.connected: 

-

202 return self.wrapped_protocol.dataReceived(data) 

-

203 

-

204 # otherwise, we must still be setting up the connection: send the data to the 

-

205 # setup client 

-

206 return self.http_setup_client.dataReceived(data) 

-

207 

-

208 

-

209class HTTPConnectSetupClient(http.HTTPClient): 

-

210 """HTTPClient protocol to send a CONNECT message for proxies and read the response. 

-

211 

-

212 Args: 

-

213 host (bytes): The hostname to send in the CONNECT message 

-

214 port (int): The port to send in the CONNECT message 

-

215 proxy_auth (tuple): None or tuple of (username, pasword) for HTTP basic proxy 

-

216 authentication 

-

217 """ 

-

218 

-

219 def __init__(self, host: bytes, port: int, proxy_auth: Optional[Tuple[str, str]]): 

-

220 self.host = host 

-

221 self.port = port 

-

222 self._proxy_auth = proxy_auth 

-

223 self.on_connected = defer.Deferred() 

-

224 

-

225 def connectionMade(self): 

-

226 logger.debug("Connected to proxy, sending CONNECT") 

-

227 self.sendCommand(b"CONNECT", b"%s:%d" % (self.host, self.port)) 

-

228 if self._proxy_auth is not None: 

-

229 username, password = self._proxy_auth 

-

230 # a credential pair is a urlsafe-base64-encoded pair separated by colon 

-

231 encoded_credentials = urlsafe_b64encode(f"{username}:{password}".encode()) 

-

232 self.sendHeader(b"Proxy-Authorization", b"basic " + encoded_credentials) 

-

233 self.endHeaders() 

-

234 

-

235 def handleStatus(self, version, status, message): 

-

236 logger.debug("Got Status: %s %s %s", status, message, version) 

-

237 if status != b"200": 

-

238 raise ProxyConnectError("Unexpected status on CONNECT: %s" % status) 

-

239 

-

240 def handleEndHeaders(self): 

-

241 logger.debug("End Headers") 

-

242 self.on_connected.callback(None) 

-

243 

-

244 def handleResponse(self, body): 

-

245 pass 

-
- - - diff --git a/htmlcov/sygnal_helper_proxy_proxy_asyncio_py.html b/htmlcov/sygnal_helper_proxy_proxy_asyncio_py.html deleted file mode 100644 index 28a1b944..00000000 --- a/htmlcov/sygnal_helper_proxy_proxy_asyncio_py.html +++ /dev/null @@ -1,447 +0,0 @@ - - - - - - Coverage for sygnal/helper/proxy/proxy_asyncio.py: 78% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2020 The Matrix.org Foundation C.I.C. 

-

3# 

-

4# Licensed under the Apache License, Version 2.0 (the "License"); 

-

5# you may not use this file except in compliance with the License. 

-

6# You may obtain a copy of the License at 

-

7# 

-

8# http://www.apache.org/licenses/LICENSE-2.0 

-

9# 

-

10# Unless required by applicable law or agreed to in writing, software 

-

11# distributed under the License is distributed on an "AS IS" BASIS, 

-

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

13# See the License for the specific language governing permissions and 

-

14# limitations under the License. 

-

15import asyncio 

-

16import logging 

-

17from asyncio import AbstractEventLoop, BaseTransport 

-

18from asyncio.futures import Future 

-

19from asyncio.protocols import Protocol 

-

20from asyncio.transports import Transport 

-

21from base64 import urlsafe_b64encode 

-

22from ssl import Purpose, SSLContext, create_default_context 

-

23from typing import Callable, Optional, Tuple, Union 

-

24 

-

25import attr 

-

26 

-

27from sygnal.exceptions import ProxyConnectError 

-

28from sygnal.helper.proxy import decompose_http_proxy_url 

-

29 

-

30logger = logging.getLogger(__name__) 

-

31 

-

32 

-

33class HttpConnectProtocol(asyncio.Protocol): 

-

34 """ 

-

35 This is for use with asyncio's Protocol and Transport API. 

-

36 

-

37 It performs the setup of a HTTP CONNECT proxy connection, then the calling 

-

38 code is responsible for handing over to another asyncio.Protocol. 

-

39 

-

40 For Twisted, see twisted_connectproxyclient.py instead. 

-

41 

-

42 The intended usage of this class is to use it in a protocol factory in 

-

43 `AbstractEventLoop.create_connection`, then await `wait_for_establishment` 

-

44 and hand the transport over to another protocol, potentially wrapping it in 

-

45 TLS with `AbstractEventLoop.start_tls`. 

-

46 

-

47 Once the connection is made, the `HttpConnectProtocol` is redundant and can 

-

48 be forgotten about; the protocol stack might look like: 

-

49 

-

50 before after 

-

51 +------------------+ 

-

52 | HTTP Protocol | 

-

53 +---------------------+ +------------------+ 

-

54 | HTTP Proxy Protocol | ===> | SSL/TLS Protocol | 

-

55 +---------------------+----------+------------------+ 

-

56 | Underlying TCP Transport | 

-

57 +---------------------------------------------------+ 

-

58 (assuming a proxied HTTPS connection is what you were after) 

-

59 """ 

-

60 

-

61 def __init__( 

-

62 self, 

-

63 target_hostport: Tuple[str, int], 

-

64 proxy_credentials: Optional[Tuple[str, str]], 

-

65 protocol_factory: Callable[[], Protocol], 

-

66 sslcontext: Optional[SSLContext], 

-

67 loop: Optional[AbstractEventLoop] = None, 

-

68 ): 

-

69 """ 

-

70 Args: 

-

71 target_hostport: 

-

72 The host & port of the destination that the proxy should connect 

-

73 to on your behalf. 

-

74 Examples: ('example.org', 443) 

-

75 

-

76 proxy_credentials: 

-

77 An optional (username, password) tuple of strings to pass to the proxy. 

-

78 

-

79 protocol_factory: 

-

80 A 0-argument function which, when called, returns a Protocol 

-

81 to switch over to. 

-

82 

-

83 sslcontext: 

-

84 If TLS is desired after the connection is completed, pass an 

-

85 SSLContext here, making sure it is safe for your purposes — 

-

86 see ssl.create_default_context's documentation as a starting 

-

87 point. 

-

88 

-

89 loop (optional): 

-

90 An asyncio EventLoop to use; if not provided, the default will 

-

91 be used. 

-

92 """ 

-

93 # set to True when we have called `switch_over_when_ready`. 

-

94 self._switch_over_called = False 

-

95 

-

96 # (host, port) of the target that we want a tunnel to 

-

97 self._target_hostport = target_hostport 

-

98 

-

99 # buffer for the HTTP response that comes back from the HTTP proxy 

-

100 self._response_buffer = b"" 

-

101 

-

102 # underlying transport 

-

103 self._transport: Transport = None # type: ignore 

-

104 

-

105 # the proxy's credentials as a string pair, or None 

-

106 self._proxy_credentials = proxy_credentials 

-

107 

-

108 # function of () -> Protocol, to be called once when we switch over to 

-

109 # this protocol 

-

110 self._protocol_factory = protocol_factory 

-

111 

-

112 # optional SSLContext if TLS is desired, None otherwise 

-

113 self._sslcontext = sslcontext 

-

114 

-

115 # asyncio EventLoop 

-

116 self._event_loop = loop or asyncio.get_event_loop() 

-

117 

-

118 # This future is completed when it is safe to take back control of the 

-

119 # transport. 

-

120 # It completes with leftover bytes for the next protocol. 

-

121 self._tunnel_established_future: Future[bytes] = Future() 

-

122 

-

123 async def switch_over_when_ready(self) -> Tuple[BaseTransport, Protocol]: 

-

124 """ 

-

125 Waits until we are connected to the remote (i.e. that our CONNECT 

-

126 request succeeds). 

-

127 Then constructs the requested protocol and attaches it to the transport, 

-

128 potentially wrapping it in TLS first. 

-

129 Returns: 

-

130 the transport followed by the constructed protocol that uses it 

-

131 Note: the transport may be an SSLTransport; it is not necessarily 

-

132 the same one used to communicate with the proxy directly. 

-

133 """ 

-

134 

-

135 if self._switch_over_called: 

-

136 raise RuntimeError( 

-

137 "Can only use `HttpConnectProtocol.switch_over_when_ready` once." 

-

138 ) 

-

139 self._switch_over_called = True 

-

140 

-

141 left_over_bytes = await self._tunnel_established_future 

-

142 # construct the desired protocol and hand over the transport to it 

-

143 new_protocol = self._protocol_factory() 

-

144 

-

145 if self._sslcontext: 

-

146 if left_over_bytes: 

-

147 # in TLS, the client transmits first, so this is theoretically 

-

148 # unreachable 

-

149 raise RuntimeError("Left over bytes should not occur with TLS") 

-

150 

-

151 # There is a race where the `new_protocol` may get given data before 

-

152 # we manage to call `connection_made` on it, which can lead to 

-

153 # exceptions if the protocol then tries to write to the transport 

-

154 # that is has been given yet. 

-

155 buffered_protocol = _BufferedWrapperProtocol(new_protocol) 

-

156 

-

157 # be careful not to use the `transport` ever again after passing it 

-

158 # to start_tls — we overwrite our variable with the TLS-wrapped 

-

159 # transport to avoid that! 

-

160 transport = await self._event_loop.start_tls( 

-

161 self._transport, 

-

162 buffered_protocol, 

-

163 self._sslcontext, 

-

164 server_hostname=self._target_hostport[0], 

-

165 ) 

-

166 

-

167 # start_tls does NOT call connection_made on new_protocol, so we 

-

168 # must do it ourselves 

-

169 buffered_protocol.connection_made(transport) 

-

170 else: 

-

171 # no wrapping required for non-TLS 

-

172 transport = self._transport 

-

173 # wire up transport to call `data_received` etc. on the new transport 

-

174 transport.set_protocol(new_protocol) 

-

175 # let the protocol know it has been connected to the transport 

-

176 new_protocol.connection_made(transport) 

-

177 

-

178 if left_over_bytes: 

-

179 # pass over dangling bytes if applicable 

-

180 new_protocol.data_received(left_over_bytes) 

-

181 

-

182 logger.debug("Finished switching protocol") 

-

183 

-

184 return transport, new_protocol 

-

185 

-

186 def data_received(self, data: bytes) -> None: 

-

187 super().data_received(data) 

-

188 self._response_buffer += data 

-

189 if b"\r\n\r\n" not in self._response_buffer: 

-

190 # we haven't finished the headers yet 

-

191 return 

-

192 

-

193 # The response headers are terminated by a double CRLF. 

-

194 # NB we want want 'in' instead of 'endswith' 

-

195 # as no guarantee error page (or even bytes from the target server) 

-

196 # won't come immediately. 

-

197 

-

198 # All HTTP header lines are terminated by CRLF. 

-

199 # the first line of the response headers is the Status Line 

-

200 try: 

-

201 response_header, dangling_bytes = self._response_buffer.split( 

-

202 b"\r\n\r\n", maxsplit=1 

-

203 ) 

-

204 lines = response_header.split(b"\r\n") 

-

205 status_line = lines[0] 

-

206 # maxsplit=2 denotes the number of separators, not the № items 

-

207 # StatusLine ← HTTPVersion SP StatusCode SP ReasonPhrase 

-

208 # None of the fields may contain CRLF, and only ReasonPhrase may 

-

209 # contain SP. 

-

210 [http_version, status, reason_phrase] = status_line.split(b" ", maxsplit=2) 

-

211 logger.debug( 

-

212 "CONNECT response from proxy: hv=%s, r=%s, rp=%s", 

-

213 http_version, 

-

214 status, 

-

215 reason_phrase, 

-

216 ) 

-

217 if status != b"200": 

-

218 # 200 Successful (aka Connection Established) is what we want 

-

219 # if it is not what we have, then we don't have a tunnel 

-

220 self._transport.close() 

-

221 raise ProxyConnectError( 

-

222 "Error from HTTP Proxy" 

-

223 f" whilst attempting CONNECT: {status.decode()}" 

-

224 f" ({reason_phrase.decode()}); aborting connection." 

-

225 ) 

-

226 

-

227 logger.debug("Ready to switch over protocol") 

-

228 

-

229 self._response_buffer = None # type: ignore 

-

230 # TLS doesn't seem to allow the server to talk before the client begins 

-

231 # the handshake, but plain HTTP/2 seems like the server can talk first 

-

232 # and who knows what the future holds? 

-

233 # (we may wish to use other protocols or TLS might change) 

-

234 # So we must also keep the left-over bytes to hand to the next Protocol 

-

235 self._tunnel_established_future.set_result(dangling_bytes) 

-

236 except Exception as exc: 

-

237 self._tunnel_established_future.set_exception(exc) 

-

238 

-

239 def connection_made(self, transport: BaseTransport) -> None: 

-

240 if not isinstance(transport, Transport): 

-

241 raise ValueError("transport must be a proper Transport") 

-

242 

-

243 super().connection_made(transport) 

-

244 # when we get a TCP connection to the HTTP proxy, we invoke the CONNECT 

-

245 # method on it to open a tunnelled TCP connection through the proxy to 

-

246 # the other side 

-

247 host, port = self._target_hostport 

-

248 transport.write(f"CONNECT {host}:{port} HTTP/1.0\r\n".encode()) 

-

249 if self._proxy_credentials: 

-

250 username, password = self._proxy_credentials 

-

251 # a credential pair is a urlsafe-base64-encoded pair separated by colon 

-

252 encoded_credentials = urlsafe_b64encode(f"{username}:{password}".encode()) 

-

253 transport.write( 

-

254 b"Proxy-Authorization: basic " + encoded_credentials + b"\r\n" 

-

255 ) 

-

256 # a blank line terminates the request headers 

-

257 transport.write(b"\r\n") 

-

258 

-

259 logger.debug("Initiating proxy CONNECT") 

-

260 

-

261 # now we wait ... 

-

262 self._transport = transport 

-

263 

-

264 

-

265class ProxyingEventLoopWrapper: 

-

266 """ 

-

267 This is a wrapper for an asyncio.AbstractEventLoop which intercepts calls to 

-

268 create_connection and transparently tunnels them through an HTTP CONNECT 

-

269 proxy. 

-

270 """ 

-

271 

-

272 def __init__( 

-

273 self, 

-

274 wrapped_loop: asyncio.AbstractEventLoop, 

-

275 proxy_url_str: str, 

-

276 ): 

-

277 """ 

-

278 Args: 

-

279 wrapped_loop: 

-

280 the underlying Event Loop to wrap 

-

281 proxy_url_str (str): 

-

282 The address of the HTTP proxy to use. 

-

283 Used to connect to the proxy, as well as in the `Host` request 

-

284 header to the proxy, and for the extraction of basic 

-

285 authentication credentials (if required). 

-

286 

-

287 Examples: 'http://127.0.3.200:8080' 

-

288 or 'http://user:secret@prox:8080' 

-

289 """ 

-

290 self._wrapped_loop = wrapped_loop 

-

291 self.proxy_url_str = proxy_url_str 

-

292 

-

293 async def create_connection( 

-

294 self, 

-

295 protocol_factory: Callable[[], asyncio.Protocol], 

-

296 host: str, 

-

297 port: int, 

-

298 ssl: Union[bool, SSLContext] = False, 

-

299 ): 

-

300 proxy_url_parts = decompose_http_proxy_url(self.proxy_url_str) 

-

301 

-

302 sslcontext: Optional[SSLContext] 

-

303 

-

304 if ssl: 

-

305 if isinstance(ssl, SSLContext): 

-

306 sslcontext = ssl 

-

307 else: 

-

308 sslcontext = create_default_context(Purpose.SERVER_AUTH) 

-

309 else: 

-

310 sslcontext = None 

-

311 

-

312 def make_protocol(): 

-

313 proxy_setup_protocol = HttpConnectProtocol( 

-

314 (host, port), 

-

315 proxy_url_parts.credentials, 

-

316 protocol_factory, 

-

317 sslcontext, 

-

318 loop=self._wrapped_loop, 

-

319 ) 

-

320 return proxy_setup_protocol 

-

321 

-

322 # enforced by decompose_http_proxy_url 

-

323 assert proxy_url_parts.hostname is not None 

-

324 

-

325 # create a raw TCP connection to the proxy 

-

326 # (N.B. if we want to ever use TLS to the proxy [e.g. to protect the proxy 

-

327 # credentials], we can ask this to give us a TLS connection). 

-

328 

-

329 transport, connect_protocol = await self._wrapped_loop.create_connection( 

-

330 make_protocol, proxy_url_parts.hostname, proxy_url_parts.port 

-

331 ) 

-

332 

-

333 assert isinstance(connect_protocol, HttpConnectProtocol) 

-

334 

-

335 # wait for the HTTP Proxy CONNECT sequence to complete, 

-

336 # and get the transport (which may be an SSLTransport rather than the 

-

337 # original) and user protocol. 

-

338 transport, user_protocol = await connect_protocol.switch_over_when_ready() 

-

339 

-

340 return transport, user_protocol 

-

341 

-

342 def __getattr__(self, item): 

-

343 """ 

-

344 We use this to delegate other method calls to the real EventLoop. 

-

345 """ 

-

346 return getattr(self._wrapped_loop, item) 

-

347 

-

348 

-

349@attr.s(slots=True, auto_attribs=True) 

-

350class _BufferedWrapperProtocol(Protocol): 

-

351 """Wraps a protocol to buffer any incoming data received before 

-

352 `connection_made` is called. 

-

353 """ 

-

354 

-

355 _protocol: Protocol 

-

356 _connected: bool = False 

-

357 _buffer: bytearray = attr.Factory(bytearray) 

-

358 

-

359 def connection_made(self, transport: BaseTransport): 

-

360 self._connected = True 

-

361 self._protocol.connection_made(transport) 

-

362 if self._buffer: 

-

363 self._protocol.data_received(self._buffer) 

-

364 self._buffer = bytearray() 

-

365 

-

366 def connection_lost(self, exc: Optional[Exception]): 

-

367 self._protocol.connection_lost(exc) 

-

368 

-

369 def pause_writing(self): 

-

370 self._protocol.pause_writing() 

-

371 

-

372 def resume_writing(self): 

-

373 self._protocol.resume_writing() 

-

374 

-

375 def data_received(self, data: bytes): 

-

376 if self._connected: 

-

377 self._protocol.data_received(data) 

-

378 else: 

-

379 self._buffer.extend(data) 

-

380 

-

381 def eof_received(self): 

-

382 return self._protocol.eof_received() 

-
- - - diff --git a/htmlcov/sygnal_helper_proxy_proxyagent_twisted_py.html b/htmlcov/sygnal_helper_proxy_proxyagent_twisted_py.html deleted file mode 100644 index 5a9a631c..00000000 --- a/htmlcov/sygnal_helper_proxy_proxyagent_twisted_py.html +++ /dev/null @@ -1,230 +0,0 @@ - - - - - - Coverage for sygnal/helper/proxy/proxyagent_twisted.py: 81% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2019-2020 The Matrix.org Foundation C.I.C. 

-

3# 

-

4# Licensed under the Apache License, Version 2.0 (the "License"); 

-

5# you may not use this file except in compliance with the License. 

-

6# You may obtain a copy of the License at 

-

7# 

-

8# http://www.apache.org/licenses/LICENSE-2.0 

-

9# 

-

10# Unless required by applicable law or agreed to in writing, software 

-

11# distributed under the License is distributed on an "AS IS" BASIS, 

-

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

13# See the License for the specific language governing permissions and 

-

14# limitations under the License. 

-

15 

-

16# Adapted from Synapse: 

-

17# https://github.com/matrix-org/synapse/blob/6920e58136671f086536332bdd6844dff0d4b429/synapse/http/proxyagent.py 

-

18 

-

19import logging 

-

20import re 

-

21from typing import Optional 

-

22 

-

23from twisted.internet import defer 

-

24from twisted.internet.endpoints import HostnameEndpoint, wrapClientTLS 

-

25from twisted.internet.interfaces import IStreamClientEndpoint 

-

26from twisted.python.failure import Failure 

-

27from twisted.web.client import URI, BrowserLikePolicyForHTTPS, _AgentBase 

-

28from twisted.web.error import SchemeNotSupported 

-

29from twisted.web.iweb import IAgent 

-

30from zope.interface import implementer 

-

31 

-

32from sygnal.helper.proxy import decompose_http_proxy_url 

-

33from sygnal.helper.proxy.connectproxyclient_twisted import HTTPConnectProxyEndpoint 

-

34 

-

35logger = logging.getLogger(__name__) 

-

36 

-

37_VALID_URI = re.compile(br"\A[\x21-\x7e]+\Z") 

-

38 

-

39 

-

40@implementer(IAgent) 

-

41class ProxyAgent(_AgentBase): 

-

42 """An Agent implementation which will use an HTTP proxy if one was requested 

-

43 

-

44 Args: 

-

45 reactor: twisted reactor to place outgoing 

-

46 connections. 

-

47 

-

48 contextFactory (IPolicyForHTTPS): A factory for TLS contexts, to control the 

-

49 verification parameters of OpenSSL. The default is to use a 

-

50 `BrowserLikePolicyForHTTPS`, so unless you have special 

-

51 requirements you can leave this as-is. 

-

52 

-

53 connectTimeout (float): The amount of time that this Agent will wait 

-

54 for the peer to accept a connection. 

-

55 

-

56 bindAddress (bytes): The local address for client sockets to bind to. 

-

57 

-

58 pool (HTTPConnectionPool|None): connection pool to be used. If None, a 

-

59 non-persistent pool instance will be created. 

-

60 """ 

-

61 

-

62 def __init__( 

-

63 self, 

-

64 reactor, 

-

65 contextFactory=BrowserLikePolicyForHTTPS(), 

-

66 connectTimeout=None, 

-

67 bindAddress=None, 

-

68 pool=None, 

-

69 proxy_url_str: Optional[str] = None, 

-

70 ): 

-

71 _AgentBase.__init__(self, reactor, pool) 

-

72 

-

73 self._endpoint_kwargs = {} 

-

74 if connectTimeout is not None: 

-

75 self._endpoint_kwargs["timeout"] = connectTimeout 

-

76 if bindAddress is not None: 

-

77 self._endpoint_kwargs["bindAddress"] = bindAddress 

-

78 

-

79 if proxy_url_str is not None: 

-

80 parsed_url = decompose_http_proxy_url(proxy_url_str) 

-

81 self._proxy_auth = parsed_url.credentials 

-

82 

-

83 self.proxy_endpoint = HostnameEndpoint( 

-

84 reactor, parsed_url.hostname, parsed_url.port, **self._endpoint_kwargs 

-

85 ) # type: Optional[HostnameEndpoint] 

-

86 else: 

-

87 self.proxy_endpoint = None 

-

88 

-

89 self._policy_for_https = contextFactory 

-

90 self._reactor = reactor 

-

91 

-

92 def request(self, method, uri, headers=None, bodyProducer=None): 

-

93 """ 

-

94 Issue a request to the server indicated by the given uri. 

-

95 

-

96 Supports `http` and `https` schemes. 

-

97 

-

98 An existing connection from the connection pool may be used or a new one may be 

-

99 created. 

-

100 

-

101 See also: twisted.web.iweb.IAgent.request 

-

102 

-

103 Args: 

-

104 method (bytes): The request method to use, such as `GET`, `POST`, etc 

-

105 

-

106 uri (bytes): The location of the resource to request. 

-

107 

-

108 headers (Headers|None): Extra headers to send with the request 

-

109 

-

110 bodyProducer (IBodyProducer|None): An object which can generate bytes to 

-

111 make up the body of this request (for example, the properly encoded 

-

112 contents of a file for a file upload). Or, None if the request is to 

-

113 have no body. 

-

114 

-

115 Returns: 

-

116 Deferred[IResponse]: completes when the header of the response has 

-

117 been received (regardless of the response status code). 

-

118 """ 

-

119 uri = uri.strip() 

-

120 if not _VALID_URI.match(uri): 

-

121 raise ValueError("Invalid URI {!r}".format(uri)) 

-

122 

-

123 parsed_uri = URI.fromBytes(uri) 

-

124 pool_key: tuple = (parsed_uri.scheme, parsed_uri.host, parsed_uri.port) 

-

125 request_path = parsed_uri.originForm 

-

126 

-

127 if parsed_uri.scheme == b"http" and self.proxy_endpoint: 

-

128 # Cache *all* connections under the same key, since we are only 

-

129 # connecting to a single destination, the proxy: 

-

130 pool_key = ("http-proxy", self.proxy_endpoint) 

-

131 endpoint = self.proxy_endpoint # type: IStreamClientEndpoint 

-

132 request_path = uri 

-

133 elif parsed_uri.scheme == b"https" and self.proxy_endpoint: 

-

134 endpoint = HTTPConnectProxyEndpoint( 

-

135 self._reactor, 

-

136 self.proxy_endpoint, 

-

137 parsed_uri.host, 

-

138 parsed_uri.port, 

-

139 self._proxy_auth, 

-

140 ) 

-

141 else: 

-

142 # not using a proxy 

-

143 endpoint = HostnameEndpoint( 

-

144 self._reactor, parsed_uri.host, parsed_uri.port, **self._endpoint_kwargs 

-

145 ) 

-

146 

-

147 logger.debug("Requesting %s via %s", uri, endpoint) 

-

148 

-

149 if parsed_uri.scheme == b"https": 

-

150 tls_connection_creator = self._policy_for_https.creatorForNetloc( 

-

151 parsed_uri.host, parsed_uri.port 

-

152 ) 

-

153 endpoint = wrapClientTLS(tls_connection_creator, endpoint) 

-

154 elif parsed_uri.scheme == b"http": 

-

155 pass 

-

156 else: 

-

157 return defer.fail( 

-

158 Failure( 

-

159 SchemeNotSupported("Unsupported scheme: %r" % (parsed_uri.scheme,)) 

-

160 ) 

-

161 ) 

-

162 

-

163 return self._requestWithEndpoint( 

-

164 pool_key, endpoint, method, parsed_uri, headers, bodyProducer, request_path 

-

165 ) 

-
- - - diff --git a/htmlcov/sygnal_http_py.html b/htmlcov/sygnal_http_py.html deleted file mode 100644 index 080832dd..00000000 --- a/htmlcov/sygnal_http_py.html +++ /dev/null @@ -1,423 +0,0 @@ - - - - - - Coverage for sygnal/http.py: 83% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2014 OpenMarket Ltd 

-

3# Copyright 2019 New Vector Ltd 

-

4# Copyright 2019 The Matrix.org Foundation C.I.C. 

-

5# 

-

6# Licensed under the Apache License, Version 2.0 (the "License"); 

-

7# you may not use this file except in compliance with the License. 

-

8# You may obtain a copy of the License at 

-

9# 

-

10# http://www.apache.org/licenses/LICENSE-2.0 

-

11# 

-

12# Unless required by applicable law or agreed to in writing, software 

-

13# distributed under the License is distributed on an "AS IS" BASIS, 

-

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

15# See the License for the specific language governing permissions and 

-

16# limitations under the License. 

-

17import json 

-

18import logging 

-

19import re 

-

20import sys 

-

21import time 

-

22import traceback 

-

23from uuid import uuid4 

-

24 

-

25from opentracing import Format, logs, tags 

-

26from prometheus_client import Counter, Gauge, Histogram 

-

27from twisted.internet.defer import ensureDeferred 

-

28from twisted.web import server 

-

29from twisted.web.http import ( 

-

30 combinedLogFormatter, 

-

31 datetimeToLogString, 

-

32 proxiedLogFormatter, 

-

33) 

-

34from twisted.web.resource import Resource 

-

35from twisted.web.server import NOT_DONE_YET 

-

36 

-

37from sygnal.notifications import NotificationContext 

-

38from sygnal.utils import NotificationLoggerAdapter, json_decoder 

-

39 

-

40from .exceptions import InvalidNotificationException, NotificationDispatchException 

-

41from .notifications import Notification 

-

42 

-

43logger = logging.getLogger(__name__) 

-

44 

-

45NOTIFS_RECEIVED_COUNTER = Counter( 

-

46 "sygnal_notifications_received", "Number of notification pokes received" 

-

47) 

-

48 

-

49NOTIFS_RECEIVED_DEVICE_PUSH_COUNTER = Counter( 

-

50 "sygnal_notifications_devices_received", "Number of devices been asked to push" 

-

51) 

-

52 

-

53NOTIFS_BY_PUSHKIN = Counter( 

-

54 "sygnal_per_pushkin_type", 

-

55 "Number of pushes sent via each type of pushkin", 

-

56 labelnames=["pushkin"], 

-

57) 

-

58 

-

59PUSHGATEWAY_HTTP_RESPONSES_COUNTER = Counter( 

-

60 "sygnal_pushgateway_status_codes", 

-

61 "HTTP Response Codes given on the Push Gateway API", 

-

62 labelnames=["code"], 

-

63) 

-

64 

-

65NOTIFY_HANDLE_HISTOGRAM = Histogram( 

-

66 "sygnal_notify_time", 

-

67 "Time taken to handle /notify push gateway request", 

-

68 labelnames=["code"], 

-

69) 

-

70 

-

71REQUESTS_IN_FLIGHT_GUAGE = Gauge( 

-

72 "sygnal_requests_in_flight", 

-

73 "Number of HTTP requests in flight", 

-

74 labelnames=["resource"], 

-

75) 

-

76 

-

77 

-

78class V1NotifyHandler(Resource): 

-

79 def __init__(self, sygnal): 

-

80 super().__init__() 

-

81 self.sygnal = sygnal 

-

82 

-

83 isLeaf = True 

-

84 

-

85 def _make_request_id(self): 

-

86 """ 

-

87 Generates a request ID, intended to be unique, for a request so it can 

-

88 be followed through logging. 

-

89 Returns: a request ID for the request. 

-

90 """ 

-

91 return str(uuid4()) 

-

92 

-

93 def render_POST(self, request): 

-

94 response = self._handle_request(request) 

-

95 if response != NOT_DONE_YET: 

-

96 PUSHGATEWAY_HTTP_RESPONSES_COUNTER.labels(code=request.code).inc() 

-

97 return response 

-

98 

-

99 def _handle_request(self, request): 

-

100 """ 

-

101 Actually handle the request. 

-

102 Args: 

-

103 request (Request): The request, corresponding to a POST request. 

-

104 

-

105 Returns: 

-

106 Either a str instance or NOT_DONE_YET. 

-

107 

-

108 """ 

-

109 request_id = self._make_request_id() 

-

110 header_dict = { 

-

111 k.decode(): v[0].decode() 

-

112 for k, v in request.requestHeaders.getAllRawHeaders() 

-

113 } 

-

114 

-

115 # extract OpenTracing scope from the HTTP headers 

-

116 span_ctx = self.sygnal.tracer.extract(Format.HTTP_HEADERS, header_dict) 

-

117 span_tags = { 

-

118 tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER, 

-

119 "request_id": request_id, 

-

120 } 

-

121 

-

122 root_span = self.sygnal.tracer.start_span( 

-

123 "pushgateway_v1_notify", child_of=span_ctx, tags=span_tags 

-

124 ) 

-

125 

-

126 # if this is True, we will not close the root_span at the end of this 

-

127 # function. 

-

128 root_span_accounted_for = False 

-

129 

-

130 try: 

-

131 context = NotificationContext(request_id, root_span, time.perf_counter()) 

-

132 

-

133 log = NotificationLoggerAdapter(logger, {"request_id": request_id}) 

-

134 

-

135 try: 

-

136 body = json_decoder.decode(request.content.read().decode("utf-8")) 

-

137 except Exception as exc: 

-

138 msg = "Expected JSON request body" 

-

139 log.warning(msg, exc_info=exc) 

-

140 root_span.log_kv({logs.EVENT: "error", "error.object": exc}) 

-

141 request.setResponseCode(400) 

-

142 return msg.encode() 

-

143 

-

144 if "notification" not in body or not isinstance(body["notification"], dict): 

-

145 msg = "Invalid notification: expecting object in 'notification' key" 

-

146 log.warning(msg) 

-

147 root_span.log_kv({logs.EVENT: "error", "message": msg}) 

-

148 request.setResponseCode(400) 

-

149 return msg.encode() 

-

150 

-

151 try: 

-

152 notif = Notification(body["notification"]) 

-

153 except InvalidNotificationException as e: 

-

154 log.exception("Invalid notification") 

-

155 request.setResponseCode(400) 

-

156 root_span.log_kv({logs.EVENT: "error", "error.object": e}) 

-

157 return str(e).encode() 

-

158 

-

159 if notif.event_id is not None: 

-

160 root_span.set_tag("event_id", notif.event_id) 

-

161 

-

162 # track whether the notification was passed with content 

-

163 root_span.set_tag("has_content", notif.content is not None) 

-

164 

-

165 NOTIFS_RECEIVED_COUNTER.inc() 

-

166 

-

167 if len(notif.devices) == 0: 

-

168 msg = "No devices in notification" 

-

169 log.warning(msg) 

-

170 request.setResponseCode(400) 

-

171 return msg.encode() 

-

172 

-

173 root_span_accounted_for = True 

-

174 

-

175 async def cb(): 

-

176 with REQUESTS_IN_FLIGHT_GUAGE.labels( 

-

177 self.__class__.__name__ 

-

178 ).track_inprogress(): 

-

179 await self._handle_dispatch(root_span, request, log, notif, context) 

-

180 

-

181 ensureDeferred(cb()) 

-

182 

-

183 # we have to try and send the notifications first, 

-

184 # so we can find out which ones to reject 

-

185 return NOT_DONE_YET 

-

186 except Exception as exc_val: 

-

187 root_span.set_tag(tags.ERROR, True) 

-

188 

-

189 # [2] corresponds to the traceback 

-

190 trace = traceback.format_tb(sys.exc_info()[2]) 

-

191 root_span.log_kv( 

-

192 { 

-

193 logs.EVENT: tags.ERROR, 

-

194 logs.MESSAGE: str(exc_val), 

-

195 logs.ERROR_OBJECT: exc_val, 

-

196 logs.ERROR_KIND: type(exc_val), 

-

197 logs.STACK: trace, 

-

198 } 

-

199 ) 

-

200 raise 

-

201 finally: 

-

202 if not root_span_accounted_for: 

-

203 root_span.finish() 

-

204 

-

205 def find_pushkins(self, appid): 

-

206 """Finds matching pushkins in self.sygnal.pushkins according to the appid. 

-

207 

-

208 

-

209 Args: 

-

210 appid (str): app identifier to search in self.sygnal.pushkins. 

-

211 

-

212 Returns: 

-

213 list of `Pushkin`: If it finds a specific pushkin with 

-

214 the exact app id, immediately returns it. 

-

215 Otherwise returns possible pushkins. 

-

216 """ 

-

217 # if found a specific appid, just return it as a list 

-

218 if appid in self.sygnal.pushkins: 

-

219 return [self.sygnal.pushkins[appid]] 

-

220 

-

221 result = [] 

-

222 for key, value in self.sygnal.pushkins.items(): 

-

223 # The ".+" symbol is used in place of "*" symbol 

-

224 regex = key.replace("*", ".+") 

-

225 if re.search(regex, appid): 

-

226 result.append(value) 

-

227 return result 

-

228 

-

229 async def _handle_dispatch(self, root_span, request, log, notif, context): 

-

230 """ 

-

231 Actually handle the dispatch of notifications to devices, sequentially 

-

232 for simplicity. 

-

233 

-

234 root_span: the OpenTracing span 

-

235 request: the Twisted Web Request 

-

236 log: the logger to use 

-

237 notif (Notification): the notification to dispatch 

-

238 context (NotificationContext): the context of the notification 

-

239 """ 

-

240 try: 

-

241 rejected = [] 

-

242 

-

243 for d in notif.devices: 

-

244 NOTIFS_RECEIVED_DEVICE_PUSH_COUNTER.inc() 

-

245 

-

246 appid = d.app_id 

-

247 found_pushkins = self.find_pushkins(appid) 

-

248 if len(found_pushkins) == 0: 

-

249 log.warning("Got notification for unknown app ID %s", appid) 

-

250 rejected.append(d.pushkey) 

-

251 continue 

-

252 

-

253 if len(found_pushkins) > 1: 

-

254 log.warning("Got notification for an ambigious app ID %s", appid) 

-

255 rejected.append(d.pushkey) 

-

256 continue 

-

257 

-

258 pushkin = found_pushkins[0] 

-

259 log.debug( 

-

260 "Sending push to pushkin %s for app ID %s", pushkin.name, appid 

-

261 ) 

-

262 

-

263 NOTIFS_BY_PUSHKIN.labels(pushkin.name).inc() 

-

264 

-

265 result = await pushkin.dispatch_notification(notif, d, context) 

-

266 if not isinstance(result, list): 

-

267 raise TypeError("Pushkin should return list.") 

-

268 

-

269 rejected += result 

-

270 

-

271 request.write(json.dumps({"rejected": rejected}).encode()) 

-

272 

-

273 if rejected: 

-

274 log.info( 

-

275 "Successfully delivered notifications with %d rejected pushkeys", 

-

276 len(rejected), 

-

277 ) 

-

278 except NotificationDispatchException: 

-

279 request.setResponseCode(502) 

-

280 log.warning("Failed to dispatch notification.", exc_info=True) 

-

281 except Exception: 

-

282 request.setResponseCode(500) 

-

283 log.error("Exception whilst dispatching notification.", exc_info=True) 

-

284 finally: 

-

285 if not request._disconnected: 

-

286 request.finish() 

-

287 

-

288 PUSHGATEWAY_HTTP_RESPONSES_COUNTER.labels(code=request.code).inc() 

-

289 root_span.set_tag(tags.HTTP_STATUS_CODE, request.code) 

-

290 

-

291 req_time = time.perf_counter() - context.start_time 

-

292 if req_time > 0: 

-

293 # can be negative as perf_counter() may not be monotonic 

-

294 NOTIFY_HANDLE_HISTOGRAM.labels(code=request.code).observe(req_time) 

-

295 if not 200 <= request.code < 300: 

-

296 root_span.set_tag(tags.ERROR, True) 

-

297 root_span.finish() 

-

298 

-

299 

-

300class HealthHandler(Resource): 

-

301 def render_GET(self, request): 

-

302 """ 

-

303 `/health` is used for automatic checking of whether the service is up. 

-

304 It should just return a blank 200 OK response. 

-

305 """ 

-

306 return b"" 

-

307 

-

308 

-

309class SygnalLoggedSite(server.Site): 

-

310 """ 

-

311 A subclass of Site to perform access logging in a way that makes sense for 

-

312 Sygnal. 

-

313 """ 

-

314 

-

315 def __init__(self, *args, log_formatter, **kwargs): 

-

316 super().__init__(*args, **kwargs) 

-

317 self.log_formatter = log_formatter 

-

318 self.logger = logging.getLogger("sygnal.access") 

-

319 

-

320 def log(self, request): 

-

321 """Log this request. Called by request.finish.""" 

-

322 # this also works around a bug in twisted.web.http.HTTPFactory which uses a 

-

323 # monotonic time as an epoch time. 

-

324 log_date_time = datetimeToLogString() 

-

325 line = self.log_formatter(log_date_time, request) 

-

326 self.logger.info("Handled request: %s", line) 

-

327 

-

328 

-

329class PushGatewayApiServer(object): 

-

330 def __init__(self, sygnal): 

-

331 """ 

-

332 Initialises the /_matrix/push/* (Push Gateway API) server. 

-

333 Args: 

-

334 sygnal (Sygnal): the Sygnal object 

-

335 """ 

-

336 root = Resource() 

-

337 matrix = Resource() 

-

338 push = Resource() 

-

339 v1 = Resource() 

-

340 

-

341 # Note that using plain strings here will lead to silent failure 

-

342 root.putChild(b"_matrix", matrix) 

-

343 matrix.putChild(b"push", push) 

-

344 push.putChild(b"v1", v1) 

-

345 v1.putChild(b"notify", V1NotifyHandler(sygnal)) 

-

346 

-

347 # add health 

-

348 root.putChild(b"health", HealthHandler()) 

-

349 

-

350 use_x_forwarded_for = sygnal.config["log"]["access"]["x_forwarded_for"] 

-

351 

-

352 log_formatter = ( 

-

353 proxiedLogFormatter if use_x_forwarded_for else combinedLogFormatter 

-

354 ) 

-

355 

-

356 self.site = SygnalLoggedSite( 

-

357 root, reactor=sygnal.reactor, log_formatter=log_formatter 

-

358 ) 

-
- - - diff --git a/htmlcov/sygnal_notifications_py.html b/htmlcov/sygnal_notifications_py.html deleted file mode 100644 index bb6f1a27..00000000 --- a/htmlcov/sygnal_notifications_py.html +++ /dev/null @@ -1,268 +0,0 @@ - - - - - - Coverage for sygnal/notifications.py: 91% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2014 OpenMarket Ltd 

-

3# Copyright 2019 New Vector Ltd 

-

4# Copyright 2019 The Matrix.org Foundation C.I.C. 

-

5# 

-

6# Licensed under the Apache License, Version 2.0 (the "License"); 

-

7# you may not use this file except in compliance with the License. 

-

8# You may obtain a copy of the License at 

-

9# 

-

10# http://www.apache.org/licenses/LICENSE-2.0 

-

11# 

-

12# Unless required by applicable law or agreed to in writing, software 

-

13# distributed under the License is distributed on an "AS IS" BASIS, 

-

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

15# See the License for the specific language governing permissions and 

-

16# limitations under the License. 

-

17import typing 

-

18from typing import Any, Dict, List, Optional 

-

19 

-

20from prometheus_client import Counter 

-

21 

-

22from .exceptions import InvalidNotificationException, NotificationDispatchException 

-

23 

-

24if typing.TYPE_CHECKING: 

-

25 from .sygnal import Sygnal 

-

26 

-

27 

-

28class Tweaks: 

-

29 def __init__(self, raw): 

-

30 self.sound = None 

-

31 

-

32 if "sound" in raw: 

-

33 self.sound = raw["sound"] 

-

34 

-

35 

-

36class Device: 

-

37 def __init__(self, raw): 

-

38 self.app_id = None 

-

39 self.pushkey = None 

-

40 self.pushkey_ts = 0 

-

41 self.data = None 

-

42 self.tweaks = None 

-

43 

-

44 if "app_id" not in raw: 

-

45 raise InvalidNotificationException("Device with no app_id") 

-

46 if "pushkey" not in raw: 

-

47 raise InvalidNotificationException("Device with no pushkey") 

-

48 if "pushkey_ts" in raw: 

-

49 self.pushkey_ts = raw["pushkey_ts"] 

-

50 if "tweaks" in raw: 

-

51 self.tweaks = Tweaks(raw["tweaks"]) 

-

52 else: 

-

53 self.tweaks = Tweaks({}) 

-

54 self.app_id = raw["app_id"] 

-

55 self.pushkey = raw["pushkey"] 

-

56 if "data" in raw: 

-

57 self.data = raw["data"] 

-

58 

-

59 

-

60class Counts: 

-

61 def __init__(self, raw): 

-

62 self.unread = None 

-

63 self.missed_calls = None 

-

64 

-

65 if "unread" in raw: 

-

66 self.unread = raw["unread"] 

-

67 if "missed_calls" in raw: 

-

68 self.missed_calls = raw["missed_calls"] 

-

69 

-

70 

-

71class Notification: 

-

72 def __init__(self, notif): 

-

73 # optional attributes 

-

74 self.room_name: Optional[str] = notif.get("room_name") 

-

75 self.room_alias: Optional[str] = notif.get("room_alias") 

-

76 self.prio: Optional[str] = notif.get("prio") 

-

77 self.membership: Optional[str] = notif.get("membership") 

-

78 self.sender_display_name: Optional[str] = notif.get("sender_display_name") 

-

79 self.content: Optional[Dict[str, Any]] = notif.get("content") 

-

80 self.event_id: Optional[str] = notif.get("event_id") 

-

81 self.room_id: Optional[str] = notif.get("room_id") 

-

82 self.user_is_target: Optional[bool] = notif.get("user_is_target") 

-

83 self.type: Optional[str] = notif.get("type") 

-

84 self.sender: Optional[str] = notif.get("sender") 

-

85 

-

86 if "devices" not in notif or not isinstance(notif["devices"], list): 

-

87 raise InvalidNotificationException("Expected list in 'devices' key") 

-

88 

-

89 if "counts" in notif: 

-

90 self.counts = Counts(notif["counts"]) 

-

91 else: 

-

92 self.counts = Counts({}) 

-

93 

-

94 self.devices = [Device(d) for d in notif["devices"]] 

-

95 

-

96 

-

97class Pushkin(object): 

-

98 def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]): 

-

99 self.name = name 

-

100 self.cfg = config 

-

101 self.sygnal = sygnal 

-

102 

-

103 def get_config(self, key: str, default=None): 

-

104 if key not in self.cfg: 

-

105 return default 

-

106 return self.cfg[key] 

-

107 

-

108 async def dispatch_notification( 

-

109 self, n: Notification, device: Device, context: "NotificationContext" 

-

110 ) -> List[str]: 

-

111 """ 

-

112 Args: 

-

113 n: The notification to dispatch via this pushkin 

-

114 device: The device to dispatch the notification for. 

-

115 context (NotificationContext): the request context 

-

116 

-

117 Returns: 

-

118 A list of rejected pushkeys, to be reported back to the homeserver 

-

119 """ 

-

120 pass 

-

121 

-

122 @classmethod 

-

123 async def create(cls, name: str, sygnal: "Sygnal", config: Dict[str, Any]): 

-

124 """ 

-

125 Override this if your pushkin needs to call async code in order to 

-

126 be constructed. Otherwise, it defaults to just invoking the Python-standard 

-

127 __init__ constructor. 

-

128 

-

129 Returns: 

-

130 an instance of this Pushkin 

-

131 """ 

-

132 return cls(name, sygnal, config) 

-

133 

-

134 

-

135class ConcurrencyLimitedPushkin(Pushkin): 

-

136 """ 

-

137 A subclass of Pushkin that limits the number of in-flight requests at any 

-

138 one time, so as to prevent one Pushkin pulling the whole show down. 

-

139 """ 

-

140 

-

141 # Maximum in-flight, concurrent notification dispatches that we apply by default 

-

142 # We start turning away requests after this limit is reached. 

-

143 DEFAULT_CONCURRENCY_LIMIT = 512 

-

144 

-

145 UNDERSTOOD_CONFIG_FIELDS = {"inflight_request_limit"} 

-

146 

-

147 RATELIMITING_DROPPED_REQUESTS = Counter( 

-

148 "sygnal_inflight_request_limit_drop", 

-

149 "Number of notifications dropped because the number of inflight requests" 

-

150 " exceeded the configured inflight_request_limit.", 

-

151 labelnames=["pushkin"], 

-

152 ) 

-

153 

-

154 def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str, Any]): 

-

155 super(ConcurrencyLimitedPushkin, self).__init__(name, sygnal, config) 

-

156 self._concurrent_limit = config.get( 

-

157 "inflight_request_limit", 

-

158 ConcurrencyLimitedPushkin.DEFAULT_CONCURRENCY_LIMIT, 

-

159 ) 

-

160 self._concurrent_now = 0 

-

161 

-

162 # Grab an instance of the dropped request counter given our pushkin name. 

-

163 # Note this ensures the counter appears in metrics even if it hasn't yet 

-

164 # been incremented. 

-

165 dropped_requests = ConcurrencyLimitedPushkin.RATELIMITING_DROPPED_REQUESTS 

-

166 self.dropped_requests_counter = dropped_requests.labels(pushkin=name) 

-

167 

-

168 async def dispatch_notification( 

-

169 self, n: Notification, device: Device, context: "NotificationContext" 

-

170 ) -> List[str]: 

-

171 if self._concurrent_now >= self._concurrent_limit: 

-

172 self.dropped_requests_counter.inc() 

-

173 raise NotificationDispatchException( 

-

174 "Too many in-flight requests for this pushkin. " 

-

175 "(Something is wrong and Sygnal is struggling to keep up!)" 

-

176 ) 

-

177 

-

178 self._concurrent_now += 1 

-

179 try: 

-

180 return await self._dispatch_notification_unlimited(n, device, context) 

-

181 finally: 

-

182 self._concurrent_now -= 1 

-

183 

-

184 async def _dispatch_notification_unlimited( 

-

185 self, n: Notification, device: Device, context: "NotificationContext" 

-

186 ) -> List[str]: 

-

187 # to be overridden by Pushkins! 

-

188 raise NotImplementedError 

-

189 

-

190 

-

191class NotificationContext(object): 

-

192 def __init__(self, request_id, opentracing_span, start_time): 

-

193 """ 

-

194 Args: 

-

195 request_id (str): An ID for the request, or None to have it 

-

196 generated automatically. 

-

197 opentracing_span (Span): The span for the API request triggering 

-

198 the notification. 

-

199 start_time (float): Start timer value, `time.perf_counter()` 

-

200 """ 

-

201 self.request_id = request_id 

-

202 self.opentracing_span = opentracing_span 

-

203 self.start_time = start_time 

-
- - - diff --git a/htmlcov/sygnal_sygnal_py.html b/htmlcov/sygnal_sygnal_py.html deleted file mode 100644 index 41ec9882..00000000 --- a/htmlcov/sygnal_sygnal_py.html +++ /dev/null @@ -1,436 +0,0 @@ - - - - - - Coverage for sygnal/sygnal.py: 51% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2014 OpenMarket Ltd 

-

3# Copyright 2018, 2019 New Vector Ltd 

-

4# Copyright 2019-2020 The Matrix.org Foundation C.I.C. 

-

5# 

-

6# Licensed under the Apache License, Version 2.0 (the "License"); 

-

7# you may not use this file except in compliance with the License. 

-

8# You may obtain a copy of the License at 

-

9# 

-

10# http://www.apache.org/licenses/LICENSE-2.0 

-

11# 

-

12# Unless required by applicable law or agreed to in writing, software 

-

13# distributed under the License is distributed on an "AS IS" BASIS, 

-

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

15# See the License for the specific language governing permissions and 

-

16# limitations under the License. 

-

17import copy 

-

18import importlib 

-

19import logging 

-

20import logging.config 

-

21import os 

-

22import sys 

-

23 

-

24import opentracing 

-

25import prometheus_client 

-

26import yaml 

-

27from opentracing.scope_managers.asyncio import AsyncioScopeManager 

-

28from twisted.enterprise.adbapi import ConnectionPool 

-

29from twisted.internet import asyncioreactor, defer 

-

30from twisted.internet.defer import ensureDeferred 

-

31from twisted.python import log as twisted_log 

-

32from twisted.python.failure import Failure 

-

33 

-

34from sygnal.http import PushGatewayApiServer 

-

35 

-

36logger = logging.getLogger(__name__) 

-

37 

-

38CONFIG_DEFAULTS: dict = { 

-

39 "http": {"port": 5000, "bind_addresses": ["127.0.0.1"]}, 

-

40 "log": {"setup": {}, "access": {"x_forwarded_for": False}}, 

-

41 "metrics": { 

-

42 "prometheus": {"enabled": False, "address": "127.0.0.1", "port": 8000}, 

-

43 "opentracing": { 

-

44 "enabled": False, 

-

45 "implementation": None, 

-

46 "jaeger": {}, 

-

47 "service_name": "sygnal", 

-

48 }, 

-

49 "sentry": {"enabled": False}, 

-

50 }, 

-

51 "proxy": None, 

-

52 "apps": {}, 

-

53 # This is defined so the key is known to check_config, but it will not 

-

54 # define a default value. 

-

55 "database": None, 

-

56} 

-

57 

-

58 

-

59class Sygnal(object): 

-

60 def __init__(self, config, custom_reactor, tracer=opentracing.tracer): 

-

61 """ 

-

62 Object that holds state for the entirety of a Sygnal instance. 

-

63 Args: 

-

64 config (dict): Configuration for this Sygnal 

-

65 custom_reactor: a Twisted Reactor to use. 

-

66 tracer (optional): an OpenTracing tracer. The default is the no-op tracer. 

-

67 """ 

-

68 self.config = config 

-

69 self.reactor = custom_reactor 

-

70 self.pushkins = {} 

-

71 self.tracer = tracer 

-

72 

-

73 logging_dict_config = config["log"]["setup"] 

-

74 logging.config.dictConfig(logging_dict_config) 

-

75 

-

76 logger.debug("Started logging") 

-

77 

-

78 observer = twisted_log.PythonLoggingObserver() 

-

79 observer.start() 

-

80 

-

81 proxy_url = config.get("proxy") 

-

82 if proxy_url is not None: 

-

83 logger.info("Using proxy configuration from Sygnal configuration file") 

-

84 else: 

-

85 proxy_url = os.getenv("HTTPS_PROXY") 

-

86 if proxy_url: 

-

87 logger.info( 

-

88 "Using proxy configuration from HTTPS_PROXY environment variable." 

-

89 ) 

-

90 config["proxy"] = proxy_url 

-

91 

-

92 # Old format db config 

-

93 if config.get("db") is not None: 

-

94 logger.warning( 

-

95 "Config includes the legacy 'db' option, please migrate" 

-

96 " to 'database' instead" 

-

97 ) 

-

98 config["database"] = { 

-

99 "name": "sqlite3", 

-

100 "args": {"dbfile": config["db"]["dbfile"]}, 

-

101 } 

-

102 elif config.get("database") is None: 

-

103 config["database"] = {"name": "sqlite3", "args": {"dbfile": "sygnal.db"}} 

-

104 

-

105 sentrycfg = config["metrics"]["sentry"] 

-

106 if sentrycfg["enabled"] is True: 

-

107 import sentry_sdk 

-

108 

-

109 logger.info("Initialising Sentry") 

-

110 sentry_sdk.init(sentrycfg["dsn"]) 

-

111 

-

112 promcfg = config["metrics"]["prometheus"] 

-

113 if promcfg["enabled"] is True: 

-

114 prom_addr = promcfg["address"] 

-

115 prom_port = int(promcfg["port"]) 

-

116 logger.info( 

-

117 "Starting Prometheus Server on %s port %d", prom_addr, prom_port 

-

118 ) 

-

119 

-

120 prometheus_client.start_http_server(port=prom_port, addr=prom_addr or "") 

-

121 

-

122 tracecfg = config["metrics"]["opentracing"] 

-

123 if tracecfg["enabled"] is True: 

-

124 if tracecfg["implementation"] == "jaeger": 

-

125 try: 

-

126 import jaeger_client 

-

127 

-

128 jaeger_cfg = jaeger_client.Config( 

-

129 config=tracecfg["jaeger"], 

-

130 service_name=tracecfg["service_name"], 

-

131 scope_manager=AsyncioScopeManager(), 

-

132 ) 

-

133 

-

134 self.tracer = jaeger_cfg.initialize_tracer() 

-

135 

-

136 logger.info("Enabled OpenTracing support with Jaeger") 

-

137 except ModuleNotFoundError: 

-

138 logger.critical( 

-

139 "You have asked for OpenTracing with Jaeger but do not have" 

-

140 " the Python package 'jaeger_client' installed." 

-

141 ) 

-

142 raise 

-

143 else: 

-

144 raise RuntimeError( 

-

145 "Unknown OpenTracing implementation: %s.", tracecfg["impl"] 

-

146 ) 

-

147 

-

148 db_name = config["database"]["name"] 

-

149 

-

150 if db_name == "psycopg2": 

-

151 logger.info("Using postgresql database") 

-

152 

-

153 # By default enable `cp_reconnect`. We need to fiddle with db_args in case 

-

154 # someone has explicitly set `cp_reconnect`. 

-

155 db_args = dict(config["database"].get("args", {})) 

-

156 db_args.setdefault("cp_reconnect", True) 

-

157 

-

158 self.database_engine = "postgresql" 

-

159 self.database = ConnectionPool( 

-

160 "psycopg2", cp_reactor=self.reactor, **db_args 

-

161 ) 

-

162 elif db_name == "sqlite3": 

-

163 logger.info("Using sqlite database") 

-

164 self.database_engine = "sqlite" 

-

165 self.database = ConnectionPool( 

-

166 "sqlite3", 

-

167 config["database"]["args"]["dbfile"], 

-

168 cp_reactor=self.reactor, 

-

169 cp_min=1, 

-

170 cp_max=1, 

-

171 check_same_thread=False, 

-

172 ) 

-

173 else: 

-

174 raise Exception("Unsupported database '%s'" % db_name) 

-

175 

-

176 async def _make_pushkin(self, app_name, app_config): 

-

177 """ 

-

178 Load and instantiate a pushkin. 

-

179 Args: 

-

180 app_name (str): The pushkin's app_id 

-

181 app_config (dict): The pushkin's configuration 

-

182 

-

183 Returns (Pushkin): 

-

184 A pushkin of the desired type. 

-

185 """ 

-

186 app_type = app_config["type"] 

-

187 if "." in app_type: 

-

188 kind_split = app_type.rsplit(".", 1) 

-

189 to_import = kind_split[0] 

-

190 to_construct = kind_split[1] 

-

191 else: 

-

192 to_import = f"sygnal.{app_type}pushkin" 

-

193 to_construct = f"{app_type.capitalize()}Pushkin" 

-

194 

-

195 logger.info("Importing pushkin module: %s", to_import) 

-

196 pushkin_module = importlib.import_module(to_import) 

-

197 logger.info("Creating pushkin: %s", to_construct) 

-

198 clarse = getattr(pushkin_module, to_construct) 

-

199 return await clarse.create(app_name, self, app_config) 

-

200 

-

201 async def _make_pushkins_then_start(self, port, bind_addresses, pushgateway_api): 

-

202 for app_id, app_cfg in self.config["apps"].items(): 

-

203 try: 

-

204 self.pushkins[app_id] = await self._make_pushkin(app_id, app_cfg) 

-

205 except Exception: 

-

206 logger.error( 

-

207 "Failed to load and create pushkin for kind '%s'" % app_cfg["type"] 

-

208 ) 

-

209 raise 

-

210 

-

211 if len(self.pushkins) == 0: 

-

212 raise RuntimeError( 

-

213 "No app IDs are configured. Edit sygnal.yaml to define some." 

-

214 ) 

-

215 

-

216 logger.info("Configured with app IDs: %r", self.pushkins.keys()) 

-

217 

-

218 for interface in bind_addresses: 

-

219 logger.info("Starting listening on %s port %d", interface, port) 

-

220 self.reactor.listenTCP(port, pushgateway_api.site, interface=interface) 

-

221 

-

222 def run(self): 

-

223 """ 

-

224 Attempt to run Sygnal and then exit the application. 

-

225 """ 

-

226 port = int(self.config["http"]["port"]) 

-

227 bind_addresses = self.config["http"]["bind_addresses"] 

-

228 pushgateway_api = PushGatewayApiServer(self) 

-

229 

-

230 @defer.inlineCallbacks 

-

231 def start(): 

-

232 try: 

-

233 yield ensureDeferred( 

-

234 self._make_pushkins_then_start( 

-

235 port, bind_addresses, pushgateway_api 

-

236 ) 

-

237 ) 

-

238 except Exception: 

-

239 # Print the exception and bail out. 

-

240 print("Error during startup:", file=sys.stderr) 

-

241 

-

242 # this gives better tracebacks than traceback.print_exc() 

-

243 Failure().printTraceback(file=sys.stderr) 

-

244 

-

245 if self.reactor.running: 

-

246 self.reactor.stop() 

-

247 

-

248 self.reactor.callWhenRunning(start) 

-

249 self.reactor.run() 

-

250 

-

251 

-

252def parse_config(): 

-

253 """ 

-

254 Find and load Sygnal's configuration file. 

-

255 Returns (dict): 

-

256 A loaded configuration. 

-

257 """ 

-

258 config_path = os.getenv("SYGNAL_CONF", "sygnal.yaml") 

-

259 print("Using configuration file: %s" % config_path, file=sys.stderr) 

-

260 try: 

-

261 with open(config_path) as file_handle: 

-

262 return yaml.safe_load(file_handle) 

-

263 except FileNotFoundError: 

-

264 logger.critical( 

-

265 "Could not find configuration file!\n" "Path: %s\n" "Absolute Path: %s", 

-

266 config_path, 

-

267 os.path.realpath(config_path), 

-

268 ) 

-

269 raise 

-

270 

-

271 

-

272def check_config(config): 

-

273 """ 

-

274 Lightly check the configuration and issue warnings as appropriate. 

-

275 Args: 

-

276 config: The loaded configuration. 

-

277 """ 

-

278 UNDERSTOOD_CONFIG_FIELDS = CONFIG_DEFAULTS.keys() 

-

279 

-

280 def check_section(section_name, known_keys, cfgpart=config): 

-

281 nonunderstood = set(cfgpart[section_name].keys()).difference(known_keys) 

-

282 if len(nonunderstood) > 0: 

-

283 logger.warning( 

-

284 f"The following configuration fields in '{section_name}' " 

-

285 f"are not understood: %s", 

-

286 nonunderstood, 

-

287 ) 

-

288 

-

289 nonunderstood = set(config.keys()).difference(UNDERSTOOD_CONFIG_FIELDS) 

-

290 if len(nonunderstood) > 0: 

-

291 logger.warning( 

-

292 "The following configuration sections are not understood: %s", nonunderstood 

-

293 ) 

-

294 

-

295 check_section("http", {"port", "bind_addresses"}) 

-

296 check_section("log", {"setup", "access"}) 

-

297 check_section( 

-

298 "access", {"file", "enabled", "x_forwarded_for"}, cfgpart=config["log"] 

-

299 ) 

-

300 check_section("metrics", {"opentracing", "sentry", "prometheus"}) 

-

301 check_section( 

-

302 "opentracing", 

-

303 {"enabled", "implementation", "jaeger", "service_name"}, 

-

304 cfgpart=config["metrics"], 

-

305 ) 

-

306 check_section( 

-

307 "prometheus", {"enabled", "address", "port"}, cfgpart=config["metrics"] 

-

308 ) 

-

309 check_section("sentry", {"enabled", "dsn"}, cfgpart=config["metrics"]) 

-

310 

-

311 # If 'db' is defined, it will override the 'database' config. 

-

312 if "db" in config: 

-

313 logger.warning( 

-

314 """The 'db' config field has been replaced by 'database'. 

-

315See the sample config for help.""" 

-

316 ) 

-

317 else: 

-

318 check_section("database", {"name", "args"}) 

-

319 

-

320 

-

321def merge_left_with_defaults(defaults, loaded_config): 

-

322 """ 

-

323 Merge two configurations, with one of them overriding the other. 

-

324 Args: 

-

325 defaults (dict): A configuration of defaults 

-

326 loaded_config (dict): A configuration, as loaded from disk. 

-

327 

-

328 Returns (dict): 

-

329 A merged configuration, with loaded_config preferred over defaults. 

-

330 """ 

-

331 result = defaults.copy() 

-

332 

-

333 if loaded_config is None: 

-

334 return result 

-

335 

-

336 # copy defaults or override them 

-

337 for k, v in result.items(): 

-

338 if isinstance(v, dict): 

-

339 if k in loaded_config: 

-

340 result[k] = merge_left_with_defaults(v, loaded_config[k]) 

-

341 else: 

-

342 result[k] = copy.deepcopy(v) 

-

343 elif k in loaded_config: 

-

344 result[k] = loaded_config[k] 

-

345 

-

346 # copy things with no defaults 

-

347 for k, v in loaded_config.items(): 

-

348 if k not in result: 

-

349 result[k] = v 

-

350 

-

351 return result 

-

352 

-

353 

-

354if __name__ == "__main__": 

-

355 # TODO we don't want to have to install the reactor, when we can get away with 

-

356 # it 

-

357 asyncioreactor.install() 

-

358 

-

359 # we remove the global reactor to make it evident when it has accidentally 

-

360 # been used: 

-

361 # ! twisted.internet.reactor = None 

-

362 # TODO can't do this ^ yet, since twisted.internet.task.{coiterate,cooperate} 

-

363 # (indirectly) depend on the globally-installed reactor and there's no way 

-

364 # to pass in a custom one. 

-

365 # and twisted.web.client uses twisted.internet.task.cooperate 

-

366 

-

367 config = parse_config() 

-

368 config = merge_left_with_defaults(CONFIG_DEFAULTS, config) 

-

369 check_config(config) 

-

370 sygnal = Sygnal(config, custom_reactor=asyncioreactor.AsyncioSelectorReactor()) 

-

371 sygnal.run() 

-
- - - diff --git a/htmlcov/sygnal_utils_py.html b/htmlcov/sygnal_utils_py.html deleted file mode 100644 index 5644e2c6..00000000 --- a/htmlcov/sygnal_utils_py.html +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - Coverage for sygnal/utils.py: 61% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2019 The Matrix.org Foundation C.I.C. 

-

3# 

-

4# Licensed under the Apache License, Version 2.0 (the "License"); 

-

5# you may not use this file except in compliance with the License. 

-

6# You may obtain a copy of the License at 

-

7# 

-

8# http://www.apache.org/licenses/LICENSE-2.0 

-

9# 

-

10# Unless required by applicable law or agreed to in writing, software 

-

11# distributed under the License is distributed on an "AS IS" BASIS, 

-

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

13# See the License for the specific language governing permissions and 

-

14# limitations under the License. 

-

15import json 

-

16import re 

-

17from logging import LoggerAdapter 

-

18 

-

19from twisted.internet.defer import Deferred 

-

20 

-

21 

-

22async def twisted_sleep(delay, twisted_reactor): 

-

23 """ 

-

24 Creates a Deferred which will fire in a set time. 

-

25 This allows you to `await` on it and have an async analogue to 

-

26 L{time.sleep}. 

-

27 Args: 

-

28 delay: Delay in seconds 

-

29 twisted_reactor: Reactor to use for sleeping. 

-

30 

-

31 Returns: 

-

32 a Deferred which fires in `delay` seconds. 

-

33 """ 

-

34 deferred = Deferred() 

-

35 twisted_reactor.callLater(delay, deferred.callback, None) 

-

36 await deferred 

-

37 

-

38 

-

39class NotificationLoggerAdapter(LoggerAdapter): 

-

40 def process(self, msg, kwargs): 

-

41 return f"[{self.extra['request_id']}] {msg}", kwargs 

-

42 

-

43 

-

44def glob_to_regex(glob): 

-

45 """Converts a glob to a compiled regex object. 

-

46 

-

47 The regex is anchored at the beginning and end of the string. 

-

48 

-

49 Args: 

-

50 glob (str) 

-

51 

-

52 Returns: 

-

53 re.RegexObject 

-

54 """ 

-

55 res = "" 

-

56 for c in glob: 

-

57 if c == "*": 

-

58 res = res + ".*" 

-

59 elif c == "?": 

-

60 res = res + "." 

-

61 else: 

-

62 res = res + re.escape(c) 

-

63 

-

64 # \A anchors at start of string, \Z at end of string 

-

65 return re.compile(r"\A" + res + r"\Z", re.IGNORECASE) 

-

66 

-

67 

-

68def _reject_invalid_json(val): 

-

69 """Do not allow Infinity, -Infinity, or NaN values in JSON.""" 

-

70 raise ValueError(f"Invalid JSON value: {val!r}") 

-

71 

-

72 

-

73# a custom JSON decoder which will reject Python extensions to JSON. 

-

74json_decoder = json.JSONDecoder(parse_constant=_reject_invalid_json) 

-
- - - diff --git a/htmlcov/sygnal_webpushpushkin_py.html b/htmlcov/sygnal_webpushpushkin_py.html deleted file mode 100644 index a435fdfe..00000000 --- a/htmlcov/sygnal_webpushpushkin_py.html +++ /dev/null @@ -1,465 +0,0 @@ - - - - - - Coverage for sygnal/webpushpushkin.py: 0% - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

1# -*- coding: utf-8 -*- 

-

2# Copyright 2021 The Matrix.org Foundation C.I.C. 

-

3# 

-

4# Licensed under the Apache License, Version 2.0 (the "License"); 

-

5# you may not use this file except in compliance with the License. 

-

6# You may obtain a copy of the License at 

-

7# 

-

8# http://www.apache.org/licenses/LICENSE-2.0 

-

9# 

-

10# Unless required by applicable law or agreed to in writing, software 

-

11# distributed under the License is distributed on an "AS IS" BASIS, 

-

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

-

13# See the License for the specific language governing permissions and 

-

14# limitations under the License. 

-

15import json 

-

16import logging 

-

17import os.path 

-

18from base64 import urlsafe_b64encode 

-

19from hashlib import blake2s 

-

20from io import BytesIO 

-

21from typing import List, Optional, Pattern 

-

22from urllib.parse import urlparse 

-

23 

-

24from prometheus_client import Gauge, Histogram 

-

25from py_vapid import Vapid, VapidException 

-

26from pywebpush import webpush 

-

27from twisted.internet.defer import DeferredSemaphore 

-

28from twisted.web.client import FileBodyProducer, HTTPConnectionPool, readBody 

-

29from twisted.web.http_headers import Headers 

-

30 

-

31from sygnal.helper.context_factory import ClientTLSOptionsFactory 

-

32from sygnal.helper.proxy.proxyagent_twisted import ProxyAgent 

-

33 

-

34from .exceptions import PushkinSetupException 

-

35from .notifications import ConcurrencyLimitedPushkin 

-

36from .utils import glob_to_regex 

-

37 

-

38QUEUE_TIME_HISTOGRAM = Histogram( 

-

39 "sygnal_webpush_queue_time", 

-

40 "Time taken waiting for a connection to WebPush endpoint", 

-

41) 

-

42 

-

43SEND_TIME_HISTOGRAM = Histogram( 

-

44 "sygnal_webpush_request_time", "Time taken to send HTTP request to WebPush endpoint" 

-

45) 

-

46 

-

47PENDING_REQUESTS_GAUGE = Gauge( 

-

48 "sygnal_pending_webpush_requests", 

-

49 "Number of WebPush requests waiting for a connection", 

-

50) 

-

51 

-

52ACTIVE_REQUESTS_GAUGE = Gauge( 

-

53 "sygnal_active_webpush_requests", "Number of WebPush requests in flight" 

-

54) 

-

55 

-

56logger = logging.getLogger(__name__) 

-

57 

-

58DEFAULT_MAX_CONNECTIONS = 20 

-

59DEFAULT_TTL = 15 * 60 # in seconds 

-

60# Max payload size is 4096 

-

61MAX_BODY_LENGTH = 1000 

-

62MAX_CIPHERTEXT_LENGTH = 2000 

-

63 

-

64 

-

65class WebpushPushkin(ConcurrencyLimitedPushkin): 

-

66 """ 

-

67 Pushkin that relays notifications to Google/Firebase Cloud Messaging. 

-

68 """ 

-

69 

-

70 UNDERSTOOD_CONFIG_FIELDS = { 

-

71 "type", 

-

72 "max_connections", 

-

73 "vapid_private_key", 

-

74 "vapid_contact_email", 

-

75 "allowed_endpoints", 

-

76 "ttl", 

-

77 } | ConcurrencyLimitedPushkin.UNDERSTOOD_CONFIG_FIELDS 

-

78 

-

79 def __init__(self, name, sygnal, config): 

-

80 super(WebpushPushkin, self).__init__(name, sygnal, config) 

-

81 

-

82 nonunderstood = self.cfg.keys() - self.UNDERSTOOD_CONFIG_FIELDS 

-

83 if nonunderstood: 

-

84 logger.warning( 

-

85 "The following configuration fields are not understood: %s", 

-

86 nonunderstood, 

-

87 ) 

-

88 

-

89 self.http_pool = HTTPConnectionPool(reactor=sygnal.reactor) 

-

90 self.max_connections = self.get_config( 

-

91 "max_connections", DEFAULT_MAX_CONNECTIONS 

-

92 ) 

-

93 self.connection_semaphore = DeferredSemaphore(self.max_connections) 

-

94 self.http_pool.maxPersistentPerHost = self.max_connections 

-

95 

-

96 tls_client_options_factory = ClientTLSOptionsFactory() 

-

97 

-

98 # use the Sygnal global proxy configuration 

-

99 proxy_url = sygnal.config.get("proxy") 

-

100 

-

101 self.http_agent = ProxyAgent( 

-

102 reactor=sygnal.reactor, 

-

103 pool=self.http_pool, 

-

104 contextFactory=tls_client_options_factory, 

-

105 proxy_url_str=proxy_url, 

-

106 ) 

-

107 self.http_request_factory = HttpRequestFactory() 

-

108 

-

109 self.allowed_endpoints = None # type: Optional[List[Pattern]] 

-

110 allowed_endpoints = self.get_config("allowed_endpoints") 

-

111 if allowed_endpoints: 

-

112 if not isinstance(allowed_endpoints, list): 

-

113 raise PushkinSetupException( 

-

114 "'allowed_endpoints' should be a list or not set" 

-

115 ) 

-

116 self.allowed_endpoints = list(map(glob_to_regex, allowed_endpoints)) 

-

117 privkey_filename = self.get_config("vapid_private_key") 

-

118 if not privkey_filename: 

-

119 raise PushkinSetupException("'vapid_private_key' not set in config") 

-

120 if not os.path.exists(privkey_filename): 

-

121 raise PushkinSetupException("path in 'vapid_private_key' does not exist") 

-

122 try: 

-

123 self.vapid_private_key = Vapid.from_file(private_key_file=privkey_filename) 

-

124 except VapidException as e: 

-

125 raise PushkinSetupException("invalid 'vapid_private_key' file") from e 

-

126 self.vapid_contact_email = self.get_config("vapid_contact_email") 

-

127 if not self.vapid_contact_email: 

-

128 raise PushkinSetupException("'vapid_contact_email' not set in config") 

-

129 self.ttl = self.get_config("ttl", DEFAULT_TTL) 

-

130 if not isinstance(self.ttl, int): 

-

131 raise PushkinSetupException("'ttl' must be an int if set") 

-

132 

-

133 async def _dispatch_notification_unlimited(self, n, device, context): 

-

134 p256dh = device.pushkey 

-

135 if not isinstance(device.data, dict): 

-

136 logger.warn( 

-

137 "Rejecting pushkey %s; device.data is not a dict", device.pushkey 

-

138 ) 

-

139 return [device.pushkey] 

-

140 

-

141 # drop notifications without an event id if requested, 

-

142 # see https://github.com/matrix-org/sygnal/issues/186 

-

143 if device.data.get("events_only") is True and not n.event_id: 

-

144 return [] 

-

145 

-

146 endpoint = device.data.get("endpoint") 

-

147 auth = device.data.get("auth") 

-

148 endpoint_domain = urlparse(endpoint).netloc 

-

149 if self.allowed_endpoints: 

-

150 allowed = any( 

-

151 regex.fullmatch(endpoint_domain) for regex in self.allowed_endpoints 

-

152 ) 

-

153 if not allowed: 

-

154 logger.error( 

-

155 "push gateway %s is not in allowed_endpoints, blocking request", 

-

156 endpoint_domain, 

-

157 ) 

-

158 # abort, but don't reject push key 

-

159 return [] 

-

160 

-

161 if not p256dh or not endpoint or not auth: 

-

162 logger.warn( 

-

163 "Rejecting pushkey; subscription info incomplete " 

-

164 + "(p256dh: %s, endpoint: %s, auth: %s)", 

-

165 p256dh, 

-

166 endpoint, 

-

167 auth, 

-

168 ) 

-

169 return [device.pushkey] 

-

170 

-

171 subscription_info = { 

-

172 "endpoint": endpoint, 

-

173 "keys": {"p256dh": p256dh, "auth": auth}, 

-

174 } 

-

175 payload = WebpushPushkin._build_payload(n, device) 

-

176 data = json.dumps(payload) 

-

177 

-

178 # web push only supports normal and low priority, so assume normal if absent 

-

179 low_priority = n.prio == "low" 

-

180 # allow dropping earlier notifications in the same room if requested 

-

181 topic = None 

-

182 if n.room_id and device.data.get("only_last_per_room") is True: 

-

183 # ask for a 22 byte hash, so the base64 of it is 32, 

-

184 # the limit webpush allows for the topic 

-

185 topic = urlsafe_b64encode( 

-

186 blake2s(n.room_id.encode(), digest_size=22).digest() 

-

187 ) 

-

188 

-

189 # note that webpush modifies vapid_claims, so make sure it's only used once 

-

190 vapid_claims = { 

-

191 "sub": "mailto:{}".format(self.vapid_contact_email), 

-

192 } 

-

193 # we use the semaphore to actually limit the number of concurrent 

-

194 # requests, since the HTTPConnectionPool will actually just lead to more 

-

195 # requests being created but not pooled – it does not perform limiting. 

-

196 with QUEUE_TIME_HISTOGRAM.time(): 

-

197 with PENDING_REQUESTS_GAUGE.track_inprogress(): 

-

198 await self.connection_semaphore.acquire() 

-

199 try: 

-

200 with SEND_TIME_HISTOGRAM.time(): 

-

201 with ACTIVE_REQUESTS_GAUGE.track_inprogress(): 

-

202 request = webpush( 

-

203 subscription_info=subscription_info, 

-

204 data=data, 

-

205 ttl=self.ttl, 

-

206 vapid_private_key=self.vapid_private_key, 

-

207 vapid_claims=vapid_claims, 

-

208 requests_session=self.http_request_factory, 

-

209 ) 

-

210 response = await request.execute( 

-

211 self.http_agent, low_priority, topic 

-

212 ) 

-

213 response_text = (await readBody(response)).decode() 

-

214 finally: 

-

215 self.connection_semaphore.release() 

-

216 

-

217 reject_pushkey = self._handle_response( 

-

218 response, response_text, device.pushkey, endpoint_domain 

-

219 ) 

-

220 if reject_pushkey: 

-

221 return [device.pushkey] 

-

222 return [] 

-

223 

-

224 @staticmethod 

-

225 def _build_payload(n, device): 

-

226 """ 

-

227 Build the payload data to be sent. 

-

228 

-

229 Args: 

-

230 n: Notification to build the payload for. 

-

231 device (Device): Device information to which the constructed payload 

-

232 will be sent. 

-

233 

-

234 Returns: 

-

235 JSON-compatible dict 

-

236 """ 

-

237 payload = {} 

-

238 

-

239 default_payload = device.data.get("default_payload") 

-

240 if isinstance(default_payload, dict): 

-

241 payload.update(default_payload) 

-

242 

-

243 for attr in [ 

-

244 "room_id", 

-

245 "room_name", 

-

246 "room_alias", 

-

247 "membership", 

-

248 "event_id", 

-

249 "sender", 

-

250 "sender_display_name", 

-

251 "user_is_target", 

-

252 "type", 

-

253 ]: 

-

254 value = getattr(n, attr, None) 

-

255 if value: 

-

256 payload[attr] = value 

-

257 

-

258 counts = getattr(n, "counts", None) 

-

259 if counts is not None: 

-

260 for attr in ["unread", "missed_calls"]: 

-

261 count_value = getattr(counts, attr, None) 

-

262 if count_value is not None: 

-

263 payload[attr] = count_value 

-

264 

-

265 if n.content and isinstance(n.content, dict): 

-

266 content = n.content.copy() 

-

267 # we can't show formatted_body in a notification anyway on web 

-

268 # so remove it 

-

269 content.pop("formatted_body", None) 

-

270 body = content.get("body") 

-

271 # make some attempts to not go over the max payload length 

-

272 if isinstance(body, str) and len(body) > MAX_BODY_LENGTH: 

-

273 content["body"] = body[0 : MAX_BODY_LENGTH - 1] + "…" 

-

274 ciphertext = content.get("ciphertext") 

-

275 if isinstance(ciphertext, str) and len(ciphertext) > MAX_CIPHERTEXT_LENGTH: 

-

276 content.pop("ciphertext", None) 

-

277 payload["content"] = content 

-

278 

-

279 return payload 

-

280 

-

281 def _handle_response(self, response, response_text, pushkey, endpoint_domain): 

-

282 """ 

-

283 Logs and determines the outcome of the response 

-

284 

-

285 Returns: 

-

286 Boolean whether the puskey should be rejected 

-

287 """ 

-

288 ttl_response_headers = response.headers.getRawHeaders(b"TTL") 

-

289 if ttl_response_headers: 

-

290 try: 

-

291 ttl_given = int(ttl_response_headers[0]) 

-

292 if ttl_given != self.ttl: 

-

293 logger.info( 

-

294 "requested TTL of %d to endpoint %s but got %d", 

-

295 self.ttl, 

-

296 endpoint_domain, 

-

297 ttl_given, 

-

298 ) 

-

299 except ValueError: 

-

300 pass 

-

301 # permanent errors 

-

302 if response.code == 404 or response.code == 410: 

-

303 logger.warn( 

-

304 "Rejecting pushkey %s; subscription is invalid on %s: %d: %s", 

-

305 pushkey, 

-

306 endpoint_domain, 

-

307 response.code, 

-

308 response_text, 

-

309 ) 

-

310 return True 

-

311 # and temporary ones 

-

312 if response.code >= 400: 

-

313 logger.warn( 

-

314 "webpush request failed for pushkey %s; %s responded with %d: %s", 

-

315 pushkey, 

-

316 endpoint_domain, 

-

317 response.code, 

-

318 response_text, 

-

319 ) 

-

320 elif response.code != 201: 

-

321 logger.info( 

-

322 "webpush request for pushkey %s didn't respond with 201; " 

-

323 + "%s responded with %d: %s", 

-

324 pushkey, 

-

325 endpoint_domain, 

-

326 response.code, 

-

327 response_text, 

-

328 ) 

-

329 return False 

-

330 

-

331 

-

332class HttpRequestFactory: 

-

333 """ 

-

334 Provide a post method that matches the API expected from pywebpush. 

-

335 """ 

-

336 

-

337 def post(self, endpoint, data, headers, timeout): 

-

338 """ 

-

339 Convert the requests-like API to a Twisted API call. 

-

340 

-

341 Args: 

-

342 endpoint (str): 

-

343 The full http url to post to 

-

344 data (bytes): 

-

345 the (encrypted) binary body of the request 

-

346 headers (py_vapid.CaseInsensitiveDict): 

-

347 A (costume) dictionary with the headers. 

-

348 timeout (int) 

-

349 Ignored for now 

-

350 """ 

-

351 return HttpDelayedRequest(endpoint, data, headers) 

-

352 

-

353 

-

354class HttpDelayedRequest: 

-

355 """ 

-

356 Captures the values received from pywebpush for the endpoint request. 

-

357 The request isn't immediately executed, to allow adding headers 

-

358 not supported by pywebpush, like Topic and Urgency. 

-

359 

-

360 Also provides the interface that pywebpush expects from a response object. 

-

361 pywebpush expects a synchronous API, while we use an asynchronous API. 

-

362 

-

363 To keep pywebpush happy we present it with some hardcoded values that 

-

364 make its assertions pass even though the HTTP request has not yet been 

-

365 made. 

-

366 

-

367 Attributes: 

-

368 status_code (int): 

-

369 Defined to be 200 so the pywebpush check to see if is below 202 

-

370 passes. 

-

371 text (str): 

-

372 Set to None as pywebpush references this field for its logging. 

-

373 """ 

-

374 

-

375 status_code = 200 

-

376 text = None 

-

377 

-

378 def __init__(self, endpoint, data, vapid_headers): 

-

379 self.endpoint = endpoint 

-

380 self.data = data 

-

381 self.vapid_headers = vapid_headers 

-

382 

-

383 def execute(self, http_agent, low_priority, topic): 

-

384 body_producer = FileBodyProducer(BytesIO(self.data)) 

-

385 # Convert the headers to the camelcase version. 

-

386 headers = { 

-

387 b"User-Agent": ["sygnal"], 

-

388 b"Content-Encoding": [self.vapid_headers["content-encoding"]], 

-

389 b"Authorization": [self.vapid_headers["authorization"]], 

-

390 b"TTL": [self.vapid_headers["ttl"]], 

-

391 b"Urgency": ["low" if low_priority else "normal"], 

-

392 } 

-

393 if topic: 

-

394 headers[b"Topic"] = [topic] 

-

395 return http_agent.request( 

-

396 b"POST", 

-

397 self.endpoint.encode(), 

-

398 headers=Headers(headers), 

-

399 bodyProducer=body_producer, 

-

400 ) 

-
- - - diff --git a/tox.ini b/tox.ini index 3c433e9b..66c7562c 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ usedevelop=true commands = coverage run --source=sygnal -m twisted.trial tests + coverage report --sort=cover coverage html [testenv:check_codestyle]