]> git.lizzy.rs Git - luairc.git/blob - src/irc/dcc.lua
a few todos for the dcc implementation
[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 irc_debug = require 'irc.debug'
7 local misc =      require 'irc.misc'
8 local socket =    require 'socket'
9 local coroutine = require 'coroutine'
10 local io =        require 'io'
11 local string =    require 'string'
12 -- }}}
13
14 ---
15 -- This module implements the DCC protocol. File transfers (DCC SEND) are
16 -- handled, but DCC CHAT is not, as of yet.
17 module 'irc.dcc'
18
19 -- defaults {{{
20 FIRST_PORT = 1028
21 LAST_PORT = 5000
22 -- }}}
23
24 -- private functions {{{
25 -- send_file {{{
26 -- TODO: no reason to be sending the size parameter all over the place when we
27 -- only need it in this function. also, should probably seek to the beginning
28 -- of the file before sending it.
29 --
30 -- Sends a file to a remote user, after that user has accepted our DCC SEND
31 -- invitation
32 -- @param sock        Socket to send the file on
33 -- @param file        Lua file object corresponding to the file we want to send
34 -- @param size        Size of the file to send
35 -- @param packet_size Size of the packets to send the file in
36 local function send_file(sock, file, size, packet_size)
37     local bytes = 0
38     while true do
39         local packet = file:read(packet_size)
40         if not packet then break end
41         bytes = bytes + packet:len()
42         local index = 1
43         while true do
44             sock:send(packet, index)
45             local new_bytes = misc.int_to_str(sock:receive(4))
46             if new_bytes ~= bytes then
47                 index = packet_size - bytes + new_bytes + 1
48             else
49                 break
50             end
51         end
52         if bytes >= size then break end
53         coroutine.yield(true)
54     end
55     file:close()
56     sock:close()
57     irc._unregister_socket(sock, 'w')
58     return true
59 end
60 -- }}}
61
62 -- handle_connect {{{
63 --
64 -- Handle the connection attempt by a remote user to get our file. Basically
65 -- just swaps out the server socket we were listening on for a client socket
66 -- that we can send data on
67 -- @param ssock Server socket that the remote user connected to
68 -- @param file  Lua file object corresponding to the file we want to send
69 -- @param size  Size of the file to send
70 -- @param packet_size Size of the packets to send the file in
71 local function handle_connect(ssock, file, size, packet_size)
72     packet_size = packet_size or 1024
73     local sock = ssock:accept()
74     sock:settimeout(0.1)
75     ssock:close()
76     irc._unregister_socket(ssock, 'r')
77     irc._register_socket(sock, 'w',
78                          coroutine.wrap(function(sock)
79                              return send_file(sock, file, size, packet_size)
80                          end))
81     return true
82 end
83 -- }}}
84
85 -- accept_file {{{
86 --
87 -- Accepts a file from a remote user which has offered it to us.
88 -- @param sock        Socket to receive the file on
89 -- @param file        Lua file object corresponding to the file we want to save
90 -- @param size        Size of the file we are receiving
91 -- @param packet_size Size of the packets to receive the file in
92 local function accept_file(sock, file, size, packet_size)
93     local bytes = 0
94     while true do
95         local packet, err, partial_packet = sock:receive(packet_size)
96         if not packet and err == "timeout" then packet = partial_packet end
97         if not packet then break end
98         if packet:len() == 0 then break end
99         bytes = bytes + packet:len()
100         sock:send(misc.str_to_int(bytes))
101         file:write(packet)
102         coroutine.yield(true)
103     end
104     file:close()
105     sock:close()
106     irc._unregister_socket(sock, 'r')
107     return true
108 end
109 -- }}}
110 -- }}}
111
112 -- public functions {{{
113 -- send {{{
114 ---
115 -- Offers a file to a remote user.
116 -- @param nick     User to offer the file to
117 -- @param filename Filename to offer
118 -- @param port     Port to accept connections on (optional, defaults to
119 --                 choosing an available port between FIRST_PORT and LAST_PORT
120 --                 above)
121 function send(nick, filename, port)
122     port = port or FIRST_PORT
123     local sock = base.assert(socket.tcp())
124     repeat
125         err, msg = sock:bind('*', port)
126         port = port + 1
127     until msg ~= "address already in use" and port <= LAST_PORT + 1
128     base.assert(err, msg)
129     base.assert(sock:listen(1))
130     local ip = misc.ip_str_to_int(irc.get_ip())
131     local file = base.assert(io.open(filename))
132     local size = file:seek("end")
133     file:seek("set")
134     irc._register_socket(sock, 'r',
135                          coroutine.wrap(function(sock)
136                              return handle_connect(sock, file, size)
137                          end))
138     filename = misc.basename(filename)
139     if filename:find(" ") then filename = '"' .. filename .. '"' end
140     irc.send("PRIVMSG", nick, {"DCC SEND " .. filename .. " " ..
141              ip .. " " .. port - 1 .. " " .. size})
142 end
143 -- }}}
144
145 -- accept {{{
146 -- TODO: this shouldn't be a public function
147 --
148 -- Accepts a file offer from a remote user. Called when the on_dcc callback
149 -- retuns true.
150 -- @param filename    Name to save the file as
151 -- @param address     IP address of the remote user
152 -- @param port        Port to connect to at the remote user
153 -- @param size        Size of the file that the remote user is offering
154 -- @param packet_size Size of the packets the remote user will be sending
155 function accept(filename, address, port, size, packet_size)
156     packet_size = packet_size or 1024
157     local sock = base.assert(socket.tcp())
158     base.assert(sock:connect(misc.ip_int_to_str(address), port))
159     sock:settimeout(0.1)
160     local file = base.assert(io.open(misc.get_unique_filename(filename), "w"))
161     irc._register_socket(sock, 'r',
162                          coroutine.wrap(function(sock)
163                              return accept_file(sock, file, size, packet_size)
164                          end))
165 end
166 -- }}}
167 -- }}}