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