| 1 | -- Prosody IM |
| 2 | -- Copyright (C) 2009 Tobias Markmann |
| 3 | -- |
| 4 | -- This project is MIT/X11 licensed. Please see the |
| 5 | -- COPYING file in the source package for more information. |
| 6 | -- |
| 7 | |
| 8 | local st = require "util.stanza"; |
| 9 | local zlib = require "zlib"; |
| 10 | local pcall = pcall; |
| 11 | local tostring = tostring; |
| 12 | |
| 13 | local xmlns_compression_feature = "http://jabber.org/features/compress" |
| 14 | local xmlns_compression_protocol = "http://jabber.org/protocol/compress" |
| 15 | local xmlns_stream = "http://etherx.jabber.org/streams"; |
| 16 | local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up(); |
| 17 | local add_filter = require "util.filters".add_filter; |
| 18 | |
| 19 | local compression_level = module:get_option("compression_level"); |
| 20 | -- if not defined assume admin wants best compression |
| 21 | if compression_level == nil then compression_level = 9 end; |
| 22 | |
| 23 | |
| 24 | compression_level = tonumber(compression_level); |
| 25 | if not compression_level or compression_level < 1 or compression_level > 9 then |
| 26 | module:log("warn", "Invalid compression level in config: %s", tostring(compression_level)); |
| 27 | module:log("warn", "Module loading aborted. Compression won't be available."); |
| 28 | return; |
| 29 | end |
| 30 | |
| 31 | module:hook("stream-features", function(event) |
| 32 | local origin, features = event.origin, event.features; |
| 33 | if not origin.compressed then |
| 34 | -- FIXME only advertise compression support when TLS layer has no compression enabled |
| 35 | features:add_child(compression_stream_feature); |
| 36 | end |
| 37 | end); |
| 38 | |
| 39 | module:hook("s2s-stream-features", function(event) |
| 40 | local origin, features = event.origin, event.features; |
| 41 | -- FIXME only advertise compression support when TLS layer has no compression enabled |
| 42 | if not origin.compressed then |
| 43 | features:add_child(compression_stream_feature); |
| 44 | end |
| 45 | end); |
| 46 | |
| 47 | -- Hook to activate compression if remote server supports it. |
| 48 | module:hook_stanza(xmlns_stream, "features", |
| 49 | function (session, stanza) |
| 50 | if not session.compressed then |
| 51 | -- does remote server support compression? |
| 52 | local comp_st = stanza:child_with_name("compression"); |
| 53 | if comp_st then |
| 54 | -- do we support the mechanism |
| 55 | for a in comp_st:children() do |
| 56 | local algorithm = a[1] |
| 57 | if algorithm == "zlib" then |
| 58 | session.sends2s(st.stanza("compress", {xmlns=xmlns_compression_protocol}):tag("method"):text("zlib")) |
| 59 | session.log("debug", "Enabled compression using zlib.") |
| 60 | return true; |
| 61 | end |
| 62 | end |
| 63 | session.log("debug", "Remote server supports no compression algorithm we support.") |
| 64 | end |
| 65 | end |
| 66 | end |
| 67 | , 250); |
| 68 | |
| 69 | |
| 70 | -- returns either nil or a fully functional ready to use inflate stream |
| 71 | local function get_deflate_stream(session) |
| 72 | local status, deflate_stream = pcall(zlib.deflate, compression_level); |
| 73 | if status == false then |
| 74 | local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); |
| 75 | (session.sends2s or session.send)(error_st); |
| 76 | session.log("error", "Failed to create zlib.deflate filter."); |
| 77 | module:log("error", "%s", tostring(deflate_stream)); |
| 78 | return |
| 79 | end |
| 80 | return deflate_stream |
| 81 | end |
| 82 | |
| 83 | -- returns either nil or a fully functional ready to use inflate stream |
| 84 | local function get_inflate_stream(session) |
| 85 | local status, inflate_stream = pcall(zlib.inflate); |
| 86 | if status == false then |
| 87 | local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); |
| 88 | (session.sends2s or session.send)(error_st); |
| 89 | session.log("error", "Failed to create zlib.inflate filter."); |
| 90 | module:log("error", "%s", tostring(inflate_stream)); |
| 91 | return |
| 92 | end |
| 93 | return inflate_stream |
| 94 | end |
| 95 | |
| 96 | -- setup compression for a stream |
| 97 | local function setup_compression(session, deflate_stream) |
| 98 | add_filter(session, "bytes/out", function(t) |
| 99 | local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync'); |
| 100 | if status == false then |
| 101 | module:log("warn", "%s", tostring(compressed)); |
| 102 | session:close({ |
| 103 | condition = "undefined-condition"; |
| 104 | text = compressed; |
| 105 | extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); |
| 106 | }); |
| 107 | return; |
| 108 | end |
| 109 | return compressed; |
| 110 | end); |
| 111 | end |
| 112 | |
| 113 | -- setup decompression for a stream |
| 114 | local function setup_decompression(session, inflate_stream) |
| 115 | add_filter(session, "bytes/in", function(data) |
| 116 | local status, decompressed, eof = pcall(inflate_stream, data); |
| 117 | if status == false then |
| 118 | module:log("warn", "%s", tostring(decompressed)); |
| 119 | session:close({ |
| 120 | condition = "undefined-condition"; |
| 121 | text = decompressed; |
| 122 | extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); |
| 123 | }); |
| 124 | return; |
| 125 | end |
| 126 | return decompressed; |
| 127 | end); |
| 128 | end |
| 129 | |
| 130 | module:hook("stanza/http://jabber.org/protocol/compress:compressed", function(event) |
| 131 | local session = event.origin; |
| 132 | |
| 133 | if session.type == "s2sout_unauthed" or session.type == "s2sout" then |
| 134 | session.log("debug", "Activating compression...") |
| 135 | -- create deflate and inflate streams |
| 136 | local deflate_stream = get_deflate_stream(session); |
| 137 | if not deflate_stream then return true; end |
| 138 | |
| 139 | local inflate_stream = get_inflate_stream(session); |
| 140 | if not inflate_stream then return true; end |
| 141 | |
| 142 | -- setup compression for session.w |
| 143 | setup_compression(session, deflate_stream); |
| 144 | |
| 145 | -- setup decompression for session.data |
| 146 | setup_decompression(session, inflate_stream); |
| 147 | session:reset_stream(); |
| 148 | local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams", |
| 149 | ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host}; |
| 150 | session.sends2s("<?xml version='1.0'?>"); |
| 151 | session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag()); |
| 152 | session.compressed = true; |
| 153 | return true; |
| 154 | end |
| 155 | end); |
| 156 | |
| 157 | module:hook("stanza/http://jabber.org/protocol/compress:compress", function(event) |
| 158 | local session, stanza = event.origin, event.stanza; |
| 159 | |
| 160 | if session.type == "c2s" or session.type == "s2sin" or session.type == "c2s_unauthed" or session.type == "s2sin_unauthed" then |
| 161 | -- fail if we are already compressed |
| 162 | if session.compressed then |
| 163 | local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); |
| 164 | (session.sends2s or session.send)(error_st); |
| 165 | session.log("debug", "Client tried to establish another compression layer."); |
| 166 | return true; |
| 167 | end |
| 168 | |
| 169 | -- checking if the compression method is supported |
| 170 | local method = stanza:child_with_name("method"); |
| 171 | method = method and (method[1] or ""); |
| 172 | if method == "zlib" then |
| 173 | session.log("debug", "zlib compression enabled."); |
| 174 | |
| 175 | -- create deflate and inflate streams |
| 176 | local deflate_stream = get_deflate_stream(session); |
| 177 | if not deflate_stream then return true; end |
| 178 | |
| 179 | local inflate_stream = get_inflate_stream(session); |
| 180 | if not inflate_stream then return true; end |
| 181 | |
| 182 | (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol})); |
| 183 | session:reset_stream(); |
| 184 | |
| 185 | -- setup compression for session.w |
| 186 | setup_compression(session, deflate_stream); |
| 187 | |
| 188 | -- setup decompression for session.data |
| 189 | setup_decompression(session, inflate_stream); |
| 190 | |
| 191 | session.compressed = true; |
| 192 | elseif method then |
| 193 | session.log("debug", "%s compression selected, but we don't support it.", tostring(method)); |
| 194 | local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method"); |
| 195 | (session.sends2s or session.send)(error_st); |
| 196 | else |
| 197 | (session.sends2s or session.send)(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed")); |
| 198 | end |
| 199 | return true; |
| 200 | end |
| 201 | end); |