]> git.lizzy.rs Git - luairc.git/blob - src/irc/dcc.lua
b4db9d11990c771cc88cb896a1af3993eee3748c
[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 -- send_file {{{
28 --
29 -- Sends a file to a remote user, after that user has accepted our DCC SEND
30 -- invitation
31 -- @param sock        Socket to send the file on
32 -- @param file        Lua file object corresponding to the file we want to send
33 -- @param packet_size Size of the packets to send the file in
34 local function send_file(sock, file, packet_size)
35     local bytes = 0
36     while true do
37         local packet = file:read(packet_size)
38         if not packet then break end
39         bytes = bytes + packet:len()
40         local index = 1
41         while true do
42             local skip = false
43             sock:send(packet, index)
44             local new_bytes, err = sock:receive(4)
45             if not new_bytes then
46                 if err == "timeout" then
47                     skip = true
48                 else
49                     irc_debug._warn(err)
50                     break
51                 end
52             else
53                 new_bytes = misc._int_to_str(new_bytes)
54             end
55             if not skip then
56                 if new_bytes ~= bytes then
57                     index = packet_size - bytes + new_bytes + 1
58                 else
59                     break
60                 end
61             end
62         end
63         coroutine.yield(true)
64     end
65     file:close()
66     sock:close()
67     irc._unregister_socket(sock, 'w')
68     return true
69 end
70 -- }}}
71
72 -- handle_connect {{{
73 --
74 -- Handle the connection attempt by a remote user to get our file. Basically
75 -- just swaps out the server socket we were listening on for a client socket
76 -- that we can send data on
77 -- @param ssock Server socket that the remote user connected to
78 -- @param file  Lua file object corresponding to the file we want to send
79 -- @param packet_size Size of the packets to send the file in
80 local function handle_connect(ssock, file, packet_size)
81     packet_size = packet_size or 1024
82     local sock = ssock:accept()
83     sock:settimeout(0.1)
84     ssock:close()
85     irc._unregister_socket(ssock, 'r')
86     irc._register_socket(sock, 'w',
87                          coroutine.wrap(function(sock)
88                              return send_file(sock, file, packet_size)
89                          end))
90     return true
91 end
92 -- }}}
93
94 -- accept_file {{{
95 --
96 -- Accepts a file from a remote user which has offered it to us.
97 -- @param sock        Socket to receive the file on
98 -- @param file        Lua file object corresponding to the file we want to save
99 -- @param packet_size Size of the packets to receive the file in
100 local function accept_file(sock, file, packet_size)
101     local bytes = 0
102     while true do
103         local packet, err, partial_packet = sock:receive(packet_size)
104         if not packet and err == "timeout" then packet = partial_packet end
105         if not packet then break end
106         if packet:len() == 0 then break end
107         bytes = bytes + packet:len()
108         sock:send(misc._str_to_int(bytes))
109         file:write(packet)
110         coroutine.yield(true)
111     end
112     file:close()
113     sock:close()
114     irc._unregister_socket(sock, 'r')
115     return true
116 end
117 -- }}}
118 -- }}}
119
120 -- internal functions {{{
121 -- _accept {{{
122 --
123 -- Accepts a file offer from a remote user. Called when the on_dcc callback
124 -- retuns true.
125 -- @param filename    Name to save the file as
126 -- @param address     IP address of the remote user
127 -- @param port        Port to connect to at the remote user
128 -- @param packet_size Size of the packets the remote user will be sending
129 function _accept(filename, address, port, packet_size)
130     packet_size = packet_size or 1024
131     local sock = base.assert(socket.tcp())
132     base.assert(sock:connect(misc._ip_int_to_str(address), port))
133     sock:settimeout(0.1)
134     local file = base.assert(io.open(misc._get_unique_filename(filename), "w"))
135     irc._register_socket(sock, 'r',
136                          coroutine.wrap(function(sock)
137                              return accept_file(sock, file, packet_size)
138                          end))
139 end
140 -- }}}
141 -- }}}
142
143 -- public functions {{{
144 -- send {{{
145 ---
146 -- Offers a file to a remote user.
147 -- @param nick     User to offer the file to
148 -- @param filename Filename to offer
149 -- @param port     Port to accept connections on (optional, defaults to
150 --                 choosing an available port between FIRST_PORT and LAST_PORT
151 --                 above)
152 function send(nick, filename, port)
153     port = port or FIRST_PORT
154     local sock = base.assert(socket.tcp())
155     repeat
156         err, msg = sock:bind('*', port)
157         port = port + 1
158     until msg ~= "address already in use" and port <= LAST_PORT + 1
159     base.assert(err, msg)
160     base.assert(sock:listen(1))
161     local ip = misc._ip_str_to_int(irc.get_ip())
162     local file = base.assert(io.open(filename))
163     local size = file:seek("end")
164     file:seek("set")
165     irc._register_socket(sock, 'r',
166                          coroutine.wrap(function(sock)
167                              return handle_connect(sock, file)
168                          end))
169     filename = misc._basename(filename)
170     if filename:find(" ") then filename = '"' .. filename .. '"' end
171     irc.send("PRIVMSG", nick, c("DCC", "SEND", filename, ip, port - 1, size))
172 end
173 -- }}}
174 -- }}}