]> git.lizzy.rs Git - luairc.git/blob - src/irc/dcc.lua
Port to Lua 5.3
[luairc.git] / src / irc / dcc.lua
1 ---
2 -- Implementation of the DCC protocol
3 -- initialization {{{
4 local irc =       libs.irc
5 local ctcp =      libs.ctcp
6 local c =         ctcp._ctcp_quote
7 local irc_debug = libs.debug
8 local misc =      libs.misc
9 local socket =    libs.socket
10 -- }}}
11
12 ---
13 -- This module implements the DCC protocol. File transfers (DCC SEND) are
14 -- handled, but DCC CHAT is not, as of yet.
15 local dcc = {}
16
17 -- defaults {{{
18 dcc.FIRST_PORT = 1028
19 dcc.LAST_PORT = 5000
20 -- }}}
21
22 -- private functions {{{
23 -- debug_dcc {{{
24 --
25 -- Prints a debug message about DCC events similar to irc.debug.warn, etc.
26 -- @param msg Debug message
27 local function debug_dcc(msg)
28     irc_debug._message("DCC", msg, "\027[0;32m")
29 end
30 -- }}}
31
32 -- send_file {{{
33 --
34 -- Sends a file to a remote user, after that user has accepted our DCC SEND
35 -- invitation
36 -- @param sock        Socket to send the file on
37 -- @param file        Lua file object corresponding to the file we want to send
38 -- @param packet_size Size of the packets to send the file in
39 local function send_file(sock, file, packet_size)
40     local bytes = 0
41     while true do
42         local packet = file:read(packet_size)
43         if not packet then break end
44         bytes = bytes + packet:len()
45         local index = 1
46         while true do
47             local skip = false
48             sock:send(packet, index)
49             local new_bytes, err = sock:receive(4)
50             if not new_bytes then
51                 if err == "timeout" then
52                     skip = true
53                 else
54                     irc_debug._warn(err)
55                     break
56                 end
57             else
58                 new_bytes = misc._int_to_str(new_bytes)
59             end
60             if not skip then
61                 if new_bytes ~= bytes then
62                     index = packet_size - bytes + new_bytes + 1
63                 else
64                     break
65                 end
66             end
67         end
68         coroutine.yield(true)
69     end
70     debug_dcc("File completely sent")
71     file:close()
72     sock:close()
73     irc._unregister_socket(sock, 'w')
74     return true
75 end
76 -- }}}
77
78 -- handle_connect {{{
79 --
80 -- Handle the connection attempt by a remote user to get our file. Basically
81 -- just swaps out the server socket we were listening on for a client socket
82 -- that we can send data on
83 -- @param ssock Server socket that the remote user connected to
84 -- @param file  Lua file object corresponding to the file we want to send
85 -- @param packet_size Size of the packets to send the file in
86 local function handle_connect(ssock, file, packet_size)
87     debug_dcc("Offer accepted, beginning to send")
88     packet_size = packet_size or 1024
89     local sock = ssock:accept()
90     sock:settimeout(0.1)
91     ssock:close()
92     irc._unregister_socket(ssock, 'r')
93     irc._register_socket(sock, 'w',
94                          coroutine.wrap(function(s)
95                              return send_file(s, file, packet_size)
96                          end))
97     return true
98 end
99 -- }}}
100
101 -- accept_file {{{
102 --
103 -- Accepts a file from a remote user which has offered it to us.
104 -- @param sock        Socket to receive the file on
105 -- @param file        Lua file object corresponding to the file we want to save
106 -- @param packet_size Size of the packets to receive the file in
107 local function accept_file(sock, file, packet_size)
108     local bytes = 0
109     while true do
110         local packet, err, partial_packet = sock:receive(packet_size)
111         if not packet and err == "timeout" then packet = partial_packet end
112         if not packet then break end
113         if packet:len() == 0 then break end
114         bytes = bytes + packet:len()
115         sock:send(misc._str_to_int(bytes))
116         file:write(packet)
117         coroutine.yield(true)
118     end
119     debug_dcc("File completely received")
120     file:close()
121     sock:close()
122     irc._unregister_socket(sock, 'r')
123     return true
124 end
125 -- }}}
126 -- }}}
127
128 -- internal functions {{{
129 -- _accept {{{
130 --
131 -- Accepts a file offer from a remote user. Called when the on_dcc callback
132 -- retuns true.
133 -- @param filename    Name to save the file as
134 -- @param address     IP address of the remote user in low level int form
135 -- @param port        Port to connect to at the remote user
136 -- @param packet_size Size of the packets the remote user will be sending
137 function dcc._accept(filename, address, port, packet_size)
138     debug_dcc("Accepting a DCC SEND request from " ..  address .. ":" .. port)
139     packet_size = packet_size or 1024
140     local sock = assert(socket.tcp())
141     assert(sock:connect(address, port))
142     sock:settimeout(0.1)
143     local file = assert(io.open(misc._get_unique_filename(filename), "w"))
144     irc._register_socket(sock, 'r',
145                          coroutine.wrap(function(s)
146                              return accept_file(s, file, packet_size)
147                          end))
148 end
149 -- }}}
150 -- }}}
151
152 -- public functions {{{
153 -- send {{{
154 ---
155 -- Offers a file to a remote user.
156 -- @param nick     User to offer the file to
157 -- @param filename Filename to offer
158 -- @param port     Port to accept connections on (optional, defaults to
159 --                 choosing an available port between FIRST_PORT and LAST_PORT
160 --                 above)
161 function dcc.send(nick, filename, port)
162     port = port or dcc.FIRST_PORT
163     local sock
164     repeat
165         sock = assert(socket.tcp())
166         err, msg = sock:bind('*', port)
167         port = port + 1
168     until msg ~= "address already in use" and port <= dcc.LAST_PORT + 1
169     port = port - 1
170     assert(err, msg)
171     assert(sock:listen(1))
172     local ip = misc._ip_str_to_int(irc.get_ip())
173     local file, err = io.open(filename)
174     if not file then
175         irc_debug._warn(err)
176         sock:close()
177         return
178     end
179     local size = file:seek("end")
180     file:seek("set")
181     irc._register_socket(sock, 'r',
182                          coroutine.wrap(function(s)
183                              return handle_connect(s, file)
184                          end))
185     filename = misc._basename(filename)
186     if filename:find(" ") then filename = '"' .. filename .. '"' end
187     debug_dcc("Offering " .. filename .. " to " .. nick .. " from " ..
188               irc.get_ip() .. ":" .. port)
189     irc.send("PRIVMSG", nick, c("DCC", "SEND", filename, ip, port, size))
190 end
191 -- }}}
192 -- }}}
193
194 return dcc