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