/* * xfer-dcc.c - file transfer via DCC protocol * * Copyright (C) 2003-2023 Sébastien Helleu * * This file is part of WeeChat, the extensible chat client. * * WeeChat is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * WeeChat is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with WeeChat. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../weechat-plugin.h" #include "xfer.h" #include "xfer-config.h" #include "xfer-file.h" #include "xfer-network.h" /* * Child process for sending file with DCC protocol. */ void xfer_dcc_send_file_child (struct t_xfer *xfer) { int num_read, num_sent; static char buffer[XFER_BLOCKSIZE_MAX]; uint32_t ack; time_t last_sent, new_time, last_second, sent_ok; unsigned long long blocksize, speed_limit, sent_last_second; /* empty file? just return immediately */ if (xfer->pos >= xfer->size) { xfer_network_write_pipe (xfer, XFER_STATUS_DONE, XFER_NO_ERROR); return; } speed_limit = (unsigned long long)weechat_config_integer ( xfer_config_network_speed_limit_send); blocksize = (unsigned long long)(xfer->blocksize); if ((speed_limit > 0) && (blocksize > speed_limit * 1024)) blocksize = speed_limit * 1024; last_sent = time (NULL); last_second = last_sent; sent_ok = 0; sent_last_second = 0; while (1) { /* read DCC ACK (sent by receiver) */ if (xfer->pos > xfer->ack) { /* we should receive ACK for packets sent previously */ while (1) { num_read = recv (xfer->sock, (char *) &ack, 4, MSG_PEEK); if ((num_read < 1) && ((num_read != -1) || ((errno != EAGAIN) && (errno != EWOULDBLOCK)))) { xfer_network_write_pipe (xfer, XFER_STATUS_FAILED, XFER_ERROR_SEND_BLOCK); return; } if (num_read == 4) { recv (xfer->sock, (char *) &ack, 4, 0); xfer->ack = ntohl (ack); /* DCC send OK? */ if ((xfer->pos >= xfer->size) && (xfer->ack >= xfer->size)) { xfer_network_write_pipe (xfer, XFER_STATUS_DONE, XFER_NO_ERROR); return; } } else break; } } /* send a block to receiver */ if ((xfer->pos < xfer->size) && (xfer->fast_send || (xfer->pos <= xfer->ack))) { if ((speed_limit > 0) && (sent_last_second >= speed_limit * 1024)) { /* we're sending too fast (according to speed limit set by user) */ usleep (100); } else { lseek (xfer->file, xfer->pos, SEEK_SET); num_read = read (xfer->file, buffer, blocksize); if (num_read < 1) { xfer_network_write_pipe (xfer, XFER_STATUS_FAILED, XFER_ERROR_READ_LOCAL); return; } num_sent = send (xfer->sock, buffer, num_read, 0); if (num_sent < 0) { /* * socket is temporarily not available (receiver can't * receive amount of data we sent ?!) */ if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) usleep (1000); else { xfer_network_write_pipe (xfer, XFER_STATUS_FAILED, XFER_ERROR_SEND_BLOCK); return; } } if (num_sent > 0) { xfer->pos += (unsigned long long) num_sent; sent_last_second += (unsigned long long) num_sent; new_time = time (NULL); if ((last_sent != new_time) || ((sent_ok == 0) && (xfer->pos >= xfer->size))) { last_sent = new_time; xfer_network_write_pipe (xfer, XFER_STATUS_ACTIVE, XFER_NO_ERROR); if (xfer->pos >= xfer->size) sent_ok = new_time; } } } } else usleep (1000); new_time = time (NULL); if (new_time > last_second) { last_second = new_time; sent_last_second = 0; } /* * if send if OK since 2 seconds or more, and that no ACK was received, * then consider it's OK */ if ((sent_ok != 0) && (new_time > sent_ok + 2)) { xfer_network_write_pipe (xfer, XFER_STATUS_DONE, XFER_NO_ERROR); return; } } } /* * Sends ACK to sender using current position in file received. * * Returns: * 2: ACK sent successfully (the 4 bytes) * 1: ACK not sent, but we consider it's not a problem * 0: ACK not sent with socket error (DCC will be closed) */ int xfer_dcc_recv_file_send_ack (struct t_xfer *xfer) { int length, num_sent, total_sent; uint32_t pos; const void *ptr_buf; pos = htonl (xfer->pos); ptr_buf = &pos; length = 4; total_sent = 0; num_sent = send (xfer->sock, ptr_buf, length, 0); if (num_sent > 0) total_sent += num_sent; while (total_sent < length) { if ((num_sent == -1) && (errno != EAGAIN) && (errno != EWOULDBLOCK)) return 0; /* if we can't send ACK now, just return with partial failure code */ if (total_sent == 0) return 1; /* at least one byte has been sent, we must send whole ACK */ usleep (1000); num_sent = send (xfer->sock, ptr_buf + total_sent, length - total_sent, 0); if (num_sent > 0) total_sent += num_sent; } /* ACK successfully sent */ return 2; } /* * Reads a resumed xfer from disk for hashing. * * Returns: * 1: OK * 0: error */ int xfer_dcc_resume_hash (struct t_xfer *xfer) { char *buf; unsigned long long total_read; ssize_t length_buf, to_read, num_read; int ret, fd; total_read = 0; ret = 1; fd = 0; length_buf = 1024 * 1024; buf = malloc (length_buf); if (!buf) return 0; while (fd <= 0) { fd = open (xfer->temp_local_filename, O_RDONLY); if (fd < 0) { if (errno == EINTR) continue; fd = 0; ret = 0; break; } } if (fd) { while (total_read < xfer->start_resume) { to_read = xfer->start_resume - total_read; if (to_read > length_buf) num_read = read (fd, buf, length_buf); else num_read = read (fd, buf, to_read); if (num_read > 0) { gcry_md_write (*xfer->hash_handle, buf, num_read); total_read += num_read; } else if (num_read < 0) { if (errno == EINTR) continue; ret = 0; break; } } while (close (fd) < 0) { if (errno != EINTR) break; } } free (buf); return ret; } /* * Child process for receiving file with DCC protocol. */ void xfer_dcc_recv_file_child (struct t_xfer *xfer) { int flags, num_read, ready; static char buffer[XFER_BLOCKSIZE_MAX]; time_t last_sent, last_second, new_time; unsigned long long blocksize, pos_last_ack, speed_limit, recv_last_second; struct pollfd poll_fd; ssize_t written, total_written; unsigned char *bin_hash; char hash[9]; speed_limit = (unsigned long long)weechat_config_integer ( xfer_config_network_speed_limit_recv); blocksize = sizeof (buffer); if ((speed_limit > 0) && (blocksize > speed_limit * 1024)) blocksize = speed_limit * 1024; /* if resuming, hash the portion of the file we have */ if ((xfer->start_resume > 0) && xfer->hash_handle) { xfer_network_write_pipe (xfer, XFER_STATUS_HASHING, XFER_NO_ERROR); if (!xfer_dcc_resume_hash (xfer)) { gcry_md_close (*xfer->hash_handle); free (xfer->hash_handle); xfer->hash_handle = NULL; xfer_network_write_pipe (xfer, XFER_STATUS_HASHING, XFER_ERROR_HASH_RESUME_ERROR); } xfer_network_write_pipe (xfer, XFER_STATUS_CONNECTING, XFER_NO_ERROR); } /* first connect to sender (blocking) */ if (xfer->type == XFER_TYPE_FILE_RECV_ACTIVE) { xfer->sock = weechat_network_connect_to (xfer->proxy, xfer->remote_address, xfer->remote_address_length); if (xfer->sock == -1) { xfer_network_write_pipe (xfer, XFER_STATUS_FAILED, XFER_ERROR_CONNECT_SENDER); return; } } /* set TCP_NODELAY to be more aggressive with acks */ flags = 1; setsockopt (xfer->sock, IPPROTO_TCP, TCP_NODELAY, &flags, sizeof (flags)); /* connection is OK, change DCC status (inform parent process) */ xfer_network_write_pipe (xfer, XFER_STATUS_ACTIVE, XFER_NO_ERROR); /* make socket non-blocking */ flags = fcntl (xfer->sock, F_GETFL); if (flags == -1) flags = 0; fcntl (xfer->sock, F_SETFL, flags | O_NONBLOCK); last_sent = time (NULL); last_second = last_sent; recv_last_second = 0; pos_last_ack = 0; while (1) { /* wait until there is something to read on socket (or error) */ poll_fd.fd = xfer->sock; poll_fd.events = POLLIN; poll_fd.revents = 0; ready = poll (&poll_fd, 1, -1); if (ready <= 0) { if ((errno == EINTR) || (errno == EAGAIN)) continue; else { xfer_network_write_pipe (xfer, XFER_STATUS_FAILED, XFER_ERROR_RECV_BLOCK); return; } } /* read maximum data on socket (until nothing is available) */ while (1) { if ((speed_limit > 0) && (recv_last_second >= speed_limit * 1024)) { /* * we're receiving too fast * (according to speed limit set by user) */ usleep (100); } else { num_read = recv (xfer->sock, buffer, blocksize, 0); if (num_read == -1) { if ((errno != EAGAIN) && (errno != EWOULDBLOCK) && (errno != EINTR)) { xfer_network_write_pipe (xfer, XFER_STATUS_FAILED, XFER_ERROR_RECV_BLOCK); return; } /* * no more data available on socket: exit loop, send ACK, and * wait for new data on socket */ break; } else { if ((num_read == 0) && (xfer->pos < xfer->size)) { xfer_network_write_pipe (xfer, XFER_STATUS_FAILED, XFER_ERROR_RECV_BLOCK); return; } /* bytes received, write to disk */ total_written = 0; while (total_written < num_read) { written = write (xfer->file, buffer + total_written, num_read - total_written); if (written < 0) { if (errno == EINTR) continue; xfer_network_write_pipe (xfer, XFER_STATUS_FAILED, XFER_ERROR_WRITE_LOCAL); return; } else { if (xfer->hash_handle) { gcry_md_write (*xfer->hash_handle, buffer + total_written, written); } total_written += written; } } xfer->pos += (unsigned long long) num_read; recv_last_second += (unsigned long long) num_read; /* file received OK? */ if (xfer->pos >= xfer->size) { /* check hash and report result to pipe */ if (xfer->hash_handle) { gcry_md_final (*xfer->hash_handle); bin_hash = gcry_md_read (*xfer->hash_handle, 0); if (bin_hash) { snprintf (hash, sizeof (hash), "%.2X%.2X%.2X%.2X", bin_hash[0], bin_hash[1], bin_hash[2], bin_hash[3]); if (weechat_strcasecmp (hash, xfer->hash_target) == 0) { xfer_network_write_pipe (xfer, XFER_STATUS_HASHED, XFER_NO_ERROR); } else { xfer_network_write_pipe (xfer, XFER_STATUS_HASHED, XFER_ERROR_HASH_MISMATCH); } } } fsync (xfer->file); /* * extra delay before sending ACK, otherwise the send of ACK * may fail */ usleep (100000); /* send ACK to sender without checking return code (file OK) */ xfer_dcc_recv_file_send_ack (xfer); /* set status done and return */ xfer_network_write_pipe (xfer, XFER_STATUS_DONE, XFER_NO_ERROR); return; } /* update status of DCC (parent process) */ new_time = time (NULL); if (last_sent != new_time) { last_sent = new_time; xfer_network_write_pipe (xfer, XFER_STATUS_ACTIVE, XFER_NO_ERROR); } } } new_time = time (NULL); if (new_time > last_second) { last_second = new_time; recv_last_second = 0; } } /* send ACK to sender (if needed) */ if (xfer->send_ack && (xfer->pos > pos_last_ack)) { switch (xfer_dcc_recv_file_send_ack (xfer)) { case 0: /* send error, socket down? */ xfer_network_write_pipe (xfer, XFER_STATUS_FAILED, XFER_ERROR_SEND_ACK); return; case 1: /* send error, not fatal (buffer full?): disable ACKs */ xfer->send_ack = 0; break; case 2: /* send OK: save position in file as last ACK sent */ pos_last_ack = xfer->pos; break; } } } }