"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "src/tftp.c" between
dnsmasq-2.80.tar.gz and dnsmasq-2.81.tar.xz

About: Dnsmasq is a lightweight caching DNS forwarder and DHCP server.

tftp.c  (dnsmasq-2.80):tftp.c  (dnsmasq-2.81.tar.xz)
/* dnsmasq is Copyright (c) 2000-2018 Simon Kelley /* dnsmasq is Copyright (c) 2000-2020 Simon Kelley
This program is free software; you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 dated June, 1991, or the Free Software Foundation; version 2 dated June, 1991, or
(at your option) version 3 dated 29 June, 2007. (at your option) version 3 dated 29 June, 2007.
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "dnsmasq.h" #include "dnsmasq.h"
#ifdef HAVE_TFTP #ifdef HAVE_TFTP
static void handle_tftp(time_t now, struct tftp_transfer *transfer, ssize_t len) ;
static struct tftp_file *check_tftp_fileperm(ssize_t *len, char *prefix); static struct tftp_file *check_tftp_fileperm(ssize_t *len, char *prefix);
static void free_transfer(struct tftp_transfer *transfer); static void free_transfer(struct tftp_transfer *transfer);
static ssize_t tftp_err(int err, char *packet, char *message, char *file); static ssize_t tftp_err(int err, char *packet, char *message, char *file);
static ssize_t tftp_err_oops(char *packet, char *file); static ssize_t tftp_err_oops(char *packet, char *file);
static ssize_t get_block(char *packet, struct tftp_transfer *transfer); static ssize_t get_block(char *packet, struct tftp_transfer *transfer);
static char *next(char **p, char *end); static char *next(char **p, char *end);
static void sanitise(char *buf); static void sanitise(char *buf);
#define OP_RRQ 1 #define OP_RRQ 1
#define OP_WRQ 2 #define OP_WRQ 2
skipping to change at line 53 skipping to change at line 54
{ {
ssize_t len; ssize_t len;
char *packet = daemon->packet; char *packet = daemon->packet;
char *filename, *mode, *p, *end, *opt; char *filename, *mode, *p, *end, *opt;
union mysockaddr addr, peer; union mysockaddr addr, peer;
struct msghdr msg; struct msghdr msg;
struct iovec iov; struct iovec iov;
struct ifreq ifr; struct ifreq ifr;
int is_err = 1, if_index = 0, mtu = 0; int is_err = 1, if_index = 0, mtu = 0;
struct iname *tmp; struct iname *tmp;
struct tftp_transfer *transfer; struct tftp_transfer *transfer = NULL, **up;
int port = daemon->start_tftp_port; /* may be zero to use ephemeral port */ int port = daemon->start_tftp_port; /* may be zero to use ephemeral port */
#if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DONT) #if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DONT)
int mtuflag = IP_PMTUDISC_DONT; int mtuflag = IP_PMTUDISC_DONT;
#endif #endif
char namebuff[IF_NAMESIZE]; char namebuff[IF_NAMESIZE];
char *name = NULL; char *name = NULL;
char *prefix = daemon->tftp_prefix; char *prefix = daemon->tftp_prefix;
struct tftp_prefix *pref; struct tftp_prefix *pref;
struct all_addr addra; union all_addr addra;
#ifdef HAVE_IPV6
/* Can always get recvd interface for IPv6 */ /* Can always get recvd interface for IPv6 */
int check_dest = !option_bool(OPT_NOWILD) || listen->family == AF_INET6; int check_dest = !option_bool(OPT_NOWILD) || listen->family == AF_INET6;
#else
int check_dest = !option_bool(OPT_NOWILD);
#endif
union { union {
struct cmsghdr align; /* this ensures alignment */ struct cmsghdr align; /* this ensures alignment */
#ifdef HAVE_IPV6
char control6[CMSG_SPACE(sizeof(struct in6_pktinfo))]; char control6[CMSG_SPACE(sizeof(struct in6_pktinfo))];
#endif
#if defined(HAVE_LINUX_NETWORK) #if defined(HAVE_LINUX_NETWORK)
char control[CMSG_SPACE(sizeof(struct in_pktinfo))]; char control[CMSG_SPACE(sizeof(struct in_pktinfo))];
#elif defined(HAVE_SOLARIS_NETWORK) #elif defined(HAVE_SOLARIS_NETWORK)
char control[CMSG_SPACE(sizeof(unsigned int))]; char control[CMSG_SPACE(sizeof(struct in_addr)) +
CMSG_SPACE(sizeof(unsigned int))];
#elif defined(IP_RECVDSTADDR) && defined(IP_RECVIF) #elif defined(IP_RECVDSTADDR) && defined(IP_RECVIF)
char control[CMSG_SPACE(sizeof(struct sockaddr_dl))]; char control[CMSG_SPACE(sizeof(struct in_addr)) +
CMSG_SPACE(sizeof(struct sockaddr_dl))];
#endif #endif
} control_u; } control_u;
msg.msg_controllen = sizeof(control_u); msg.msg_controllen = sizeof(control_u);
msg.msg_control = control_u.control; msg.msg_control = control_u.control;
msg.msg_flags = 0; msg.msg_flags = 0;
msg.msg_name = &peer; msg.msg_name = &peer;
msg.msg_namelen = sizeof(peer); msg.msg_namelen = sizeof(peer);
msg.msg_iov = &iov; msg.msg_iov = &iov;
msg.msg_iovlen = 1; msg.msg_iovlen = 1;
skipping to change at line 177 skipping to change at line 174
} p; } p;
p.c = CMSG_DATA(cmptr); p.c = CMSG_DATA(cmptr);
if (cmptr->cmsg_level == IPPROTO_IP && cmptr->cmsg_type == IP_RECVDST ADDR) if (cmptr->cmsg_level == IPPROTO_IP && cmptr->cmsg_type == IP_RECVDST ADDR)
addr.in.sin_addr = *(p.a); addr.in.sin_addr = *(p.a);
else if (cmptr->cmsg_level == IPPROTO_IP && cmptr->cmsg_type == IP_RE CVIF) else if (cmptr->cmsg_level == IPPROTO_IP && cmptr->cmsg_type == IP_RE CVIF)
if_index = p.s->sdl_index; if_index = p.s->sdl_index;
} }
#endif #endif
#ifdef HAVE_IPV6
if (listen->family == AF_INET6) if (listen->family == AF_INET6)
{ {
for (cmptr = CMSG_FIRSTHDR(&msg); cmptr; cmptr = CMSG_NXTHDR(&msg, cmp tr)) for (cmptr = CMSG_FIRSTHDR(&msg); cmptr; cmptr = CMSG_NXTHDR(&msg, cmp tr))
if (cmptr->cmsg_level == IPPROTO_IPV6 && cmptr->cmsg_type == daemon- >v6pktinfo) if (cmptr->cmsg_level == IPPROTO_IPV6 && cmptr->cmsg_type == daemon- >v6pktinfo)
{ {
union { union {
unsigned char *c; unsigned char *c;
struct in6_pktinfo *p; struct in6_pktinfo *p;
} p; } p;
p.c = CMSG_DATA(cmptr); p.c = CMSG_DATA(cmptr);
addr.in6.sin6_addr = p.p->ipi6_addr; addr.in6.sin6_addr = p.p->ipi6_addr;
if_index = p.p->ipi6_ifindex; if_index = p.p->ipi6_ifindex;
} }
} }
#endif
if (!indextoname(listen->tftpfd, if_index, namebuff)) if (!indextoname(listen->tftpfd, if_index, namebuff))
return; return;
name = namebuff; name = namebuff;
addra.addr.addr4 = addr.in.sin_addr; addra.addr4 = addr.in.sin_addr;
#ifdef HAVE_IPV6
if (listen->family == AF_INET6) if (listen->family == AF_INET6)
addra.addr.addr6 = addr.in6.sin6_addr; addra.addr6 = addr.in6.sin6_addr;
#endif
if (daemon->tftp_interfaces) if (daemon->tftp_interfaces)
{ {
/* dedicated tftp interface list */ /* dedicated tftp interface list */
for (tmp = daemon->tftp_interfaces; tmp; tmp = tmp->next) for (tmp = daemon->tftp_interfaces; tmp; tmp = tmp->next)
if (tmp->name && wildcard_match(tmp->name, name)) if (tmp->name && wildcard_match(tmp->name, name))
break; break;
if (!tmp) if (!tmp)
return; return;
} }
else else
{ {
/* Do the same as DHCP */ /* Do the same as DHCP */
if (!iface_check(listen->family, &addra, name, NULL)) if (!iface_check(listen->family, &addra, name, NULL))
{ {
if (!option_bool(OPT_CLEVERBIND)) if (!option_bool(OPT_CLEVERBIND))
enumerate_interfaces(0); enumerate_interfaces(0);
if (!loopback_exception(listen->tftpfd, listen->family, &addra, nam e) && if (!loopback_exception(listen->tftpfd, listen->family, &addra, nam e) &&
!label_exception(if_index, listen->family, &addra) ) !label_exception(if_index, listen->family, &addra))
return; return;
} }
#ifdef HAVE_DHCP #ifdef HAVE_DHCP
/* allowed interfaces are the same as for DHCP */ /* allowed interfaces are the same as for DHCP */
for (tmp = daemon->dhcp_except; tmp; tmp = tmp->next) for (tmp = daemon->dhcp_except; tmp; tmp = tmp->next)
if (tmp->name && wildcard_match(tmp->name, name)) if (tmp->name && wildcard_match(tmp->name, name))
return; return;
#endif #endif
} }
skipping to change at line 250 skipping to change at line 243
mtu = ifr.ifr_mtu; mtu = ifr.ifr_mtu;
if (daemon->tftp_mtu != 0 && daemon->tftp_mtu < mtu) if (daemon->tftp_mtu != 0 && daemon->tftp_mtu < mtu)
mtu = daemon->tftp_mtu; mtu = daemon->tftp_mtu;
} }
} }
/* Failed to get interface mtu - can use configured value. */ /* Failed to get interface mtu - can use configured value. */
if (mtu == 0) if (mtu == 0)
mtu = daemon->tftp_mtu; mtu = daemon->tftp_mtu;
/* data transfer via server listening socket */
if (option_bool(OPT_SINGLE_PORT))
{
int tftp_cnt;
for (tftp_cnt = 0, transfer = daemon->tftp_trans, up = &daemon->tftp_trans
; transfer; up = &transfer->next, transfer = transfer->next)
{
tftp_cnt++;
if (sockaddr_isequal(&peer, &transfer->peer))
{
if (ntohs(*((unsigned short *)packet)) == OP_RRQ)
{
/* Handle repeated RRQ or abandoned transfer from same host and
port
by unlinking and reusing the struct transfer. */
*up = transfer->next;
break;
}
else
{
handle_tftp(now, transfer, len);
return;
}
}
}
/* Enforce simultaneous transfer limit. In non-single-port mode
this is doene by not listening on the server socket when
too many transfers are in progress. */
if (!transfer && tftp_cnt >= daemon->tftp_max)
return;
}
if (name) if (name)
{ {
/* check for per-interface prefix */ /* check for per-interface prefix */
for (pref = daemon->if_prefix; pref; pref = pref->next) for (pref = daemon->if_prefix; pref; pref = pref->next)
if (strcmp(pref->interface, name) == 0) if (strcmp(pref->interface, name) == 0)
prefix = pref->prefix; prefix = pref->prefix;
} }
if (listen->family == AF_INET) if (listen->family == AF_INET)
{ {
addr.in.sin_port = htons(port); addr.in.sin_port = htons(port);
#ifdef HAVE_SOCKADDR_SA_LEN #ifdef HAVE_SOCKADDR_SA_LEN
addr.in.sin_len = sizeof(addr.in); addr.in.sin_len = sizeof(addr.in);
#endif #endif
} }
#ifdef HAVE_IPV6
else else
{ {
addr.in6.sin6_port = htons(port); addr.in6.sin6_port = htons(port);
addr.in6.sin6_flowinfo = 0; addr.in6.sin6_flowinfo = 0;
addr.in6.sin6_scope_id = 0; addr.in6.sin6_scope_id = 0;
#ifdef HAVE_SOCKADDR_SA_LEN #ifdef HAVE_SOCKADDR_SA_LEN
addr.in6.sin6_len = sizeof(addr.in6); addr.in6.sin6_len = sizeof(addr.in6);
#endif #endif
} }
#endif
if (!(transfer = whine_malloc(sizeof(struct tftp_transfer)))) /* May reuse struct transfer from abandoned transfer in single port mode. */
if (!transfer && !(transfer = whine_malloc(sizeof(struct tftp_transfer))))
return; return;
if ((transfer->sockfd = socket(listen->family, SOCK_DGRAM, 0)) == -1) if (option_bool(OPT_SINGLE_PORT))
transfer->sockfd = listen->tftpfd;
else if ((transfer->sockfd = socket(listen->family, SOCK_DGRAM, 0)) == -1)
{ {
free(transfer); free(transfer);
return; return;
} }
transfer->peer = peer; transfer->peer = peer;
transfer->source = addra;
transfer->if_index = if_index;
transfer->timeout = now + 2; transfer->timeout = now + 2;
transfer->backoff = 1; transfer->backoff = 1;
transfer->block = 1; transfer->block = 1;
transfer->blocksize = 512; transfer->blocksize = 512;
transfer->offset = 0; transfer->offset = 0;
transfer->file = NULL; transfer->file = NULL;
transfer->opt_blocksize = transfer->opt_transize = 0; transfer->opt_blocksize = transfer->opt_transize = 0;
transfer->netascii = transfer->carrylf = 0; transfer->netascii = transfer->carrylf = 0;
prettyprint_addr(&peer, daemon->addrbuff); prettyprint_addr(&peer, daemon->addrbuff);
/* if we have a nailed-down range, iterate until we find a free one. */ /* if we have a nailed-down range, iterate until we find a free one. */
while (1) while (!option_bool(OPT_SINGLE_PORT))
{ {
if (bind(transfer->sockfd, &addr.sa, sa_len(&addr)) == -1 || if (bind(transfer->sockfd, &addr.sa, sa_len(&addr)) == -1 ||
#if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DONT) #if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DONT)
setsockopt(transfer->sockfd, IPPROTO_IP, IP_MTU_DISCOVER, &mtuflag, siz eof(mtuflag)) == -1 || setsockopt(transfer->sockfd, IPPROTO_IP, IP_MTU_DISCOVER, &mtuflag, siz eof(mtuflag)) == -1 ||
#endif #endif
!fix_fd(transfer->sockfd)) !fix_fd(transfer->sockfd))
{ {
if (errno == EADDRINUSE && daemon->start_tftp_port != 0) if (errno == EADDRINUSE && daemon->start_tftp_port != 0)
{ {
if (++port <= daemon->end_tftp_port) if (++port <= daemon->end_tftp_port)
{ {
if (listen->family == AF_INET) if (listen->family == AF_INET)
addr.in.sin_port = htons(port); addr.in.sin_port = htons(port);
#ifdef HAVE_IPV6
else else
addr.in6.sin6_port = htons(port); addr.in6.sin6_port = htons(port);
#endif
continue; continue;
} }
my_syslog(MS_TFTP | LOG_ERR, _("unable to get free port for TFTP")) ; my_syslog(MS_TFTP | LOG_ERR, _("unable to get free port for TFTP")) ;
} }
free_transfer(transfer); free_transfer(transfer);
return; return;
} }
break; break;
} }
skipping to change at line 454 skipping to change at line 482
/* check permissions and open file */ /* check permissions and open file */
if ((transfer->file = check_tftp_fileperm(&len, prefix))) if ((transfer->file = check_tftp_fileperm(&len, prefix)))
{ {
if ((len = get_block(packet, transfer)) == -1) if ((len = get_block(packet, transfer)) == -1)
len = tftp_err_oops(packet, daemon->namebuff); len = tftp_err_oops(packet, daemon->namebuff);
else else
is_err = 0; is_err = 0;
} }
} }
while (sendto(transfer->sockfd, packet, len, 0, send_from(transfer->sockfd, !option_bool(OPT_SINGLE_PORT), packet, len, &peer,
(struct sockaddr *)&peer, sa_len(&peer)) == -1 && errno == EINTR) &addra, if_index);
;
if (is_err) if (is_err)
free_transfer(transfer); free_transfer(transfer);
else else
{ {
transfer->next = daemon->tftp_trans; transfer->next = daemon->tftp_trans;
daemon->tftp_trans = transfer; daemon->tftp_trans = transfer;
} }
} }
skipping to change at line 552 skipping to change at line 579
oops: oops:
*len = tftp_err_oops(packet, namebuff); *len = tftp_err_oops(packet, namebuff);
if (fd != -1) if (fd != -1)
close(fd); close(fd);
return NULL; return NULL;
} }
void check_tftp_listeners(time_t now) void check_tftp_listeners(time_t now)
{ {
struct tftp_transfer *transfer, *tmp, **up; struct tftp_transfer *transfer, *tmp, **up;
ssize_t len;
struct ack {
unsigned short op, block;
} *mess = (struct ack *)daemon->packet;
/* Check for activity on any existing transfers */
for (transfer = daemon->tftp_trans, up = &daemon->tftp_trans; transfer; transf
er = tmp)
{
tmp = transfer->next;
prettyprint_addr(&transfer->peer, daemon->addrbuff);
/* In single port mode, all packets come via port 69 and tftp_request() */
if (!option_bool(OPT_SINGLE_PORT))
for (transfer = daemon->tftp_trans; transfer; transfer = transfer->next)
if (poll_check(transfer->sockfd, POLLIN)) if (poll_check(transfer->sockfd, POLLIN))
{ {
/* we overwrote the buffer... */ /* we overwrote the buffer... */
daemon->srv_save = NULL; daemon->srv_save = NULL;
handle_tftp(now, transfer, recv(transfer->sockfd, daemon->packet, daemo
if ((len = recv(transfer->sockfd, daemon->packet, daemon->packet_buff_s n->packet_buff_sz, 0));
z, 0)) >= (ssize_t)sizeof(struct ack))
{
if (ntohs(mess->op) == OP_ACK && ntohs(mess->block) == (unsigned sh
ort)transfer->block)
{
/* Got ack, ensure we take the (re)transmit path */
transfer->timeout = now;
transfer->backoff = 0;
if (transfer->block++ != 0)
transfer->offset += transfer->blocksize - transfer->expansion
;
}
else if (ntohs(mess->op) == OP_ERR)
{
char *p = daemon->packet + sizeof(struct ack);
char *end = daemon->packet + len;
char *err = next(&p, end);
/* Sanitise error message */
if (!err)
err = "";
else
sanitise(err);
my_syslog(MS_TFTP | LOG_ERR, _("error %d %s received from %s"),
(int)ntohs(mess->block), err,
daemon->addrbuff);
/* Got err, ensure we take abort */
transfer->timeout = now;
transfer->backoff = 100;
}
}
} }
for (transfer = daemon->tftp_trans, up = &daemon->tftp_trans; transfer; transf
er = tmp)
{
tmp = transfer->next;
if (difftime(now, transfer->timeout) >= 0.0) if (difftime(now, transfer->timeout) >= 0.0)
{ {
int endcon = 0; int endcon = 0;
ssize_t len;
/* timeout, retransmit */ /* timeout, retransmit */
transfer->timeout += 1 + (1<<transfer->backoff); transfer->timeout += 1 + (1<<(transfer->backoff/2));
/* we overwrote the buffer... */ /* we overwrote the buffer... */
daemon->srv_save = NULL; daemon->srv_save = NULL;
if ((len = get_block(daemon->packet, transfer)) == -1) if ((len = get_block(daemon->packet, transfer)) == -1)
{ {
len = tftp_err_oops(daemon->packet, transfer->file->filename); len = tftp_err_oops(daemon->packet, transfer->file->filename);
endcon = 1; endcon = 1;
} }
/* don't complain about timeout when we're awaiting the last else if (++transfer->backoff > 7)
ACK, some clients never send it */
else if (++transfer->backoff > 7 && len != 0)
{ {
endcon = 1; /* don't complain about timeout when we're awaiting the last
ACK, some clients never send it */
if ((unsigned)len == transfer->blocksize + 4)
endcon = 1;
len = 0; len = 0;
} }
if (len != 0) if (len != 0)
while(sendto(transfer->sockfd, daemon->packet, len, 0, send_from(transfer->sockfd, !option_bool(OPT_SINGLE_PORT), daemon->pa
(struct sockaddr *)&transfer->peer, sa_len(&transfer->pe cket, len,
er)) == -1 && errno == EINTR); &transfer->peer, &transfer->source, transfer->if_index);
if (endcon || len == 0) if (endcon || len == 0)
{ {
strcpy(daemon->namebuff, transfer->file->filename); strcpy(daemon->namebuff, transfer->file->filename);
sanitise(daemon->namebuff); sanitise(daemon->namebuff);
prettyprint_addr(&transfer->peer, daemon->addrbuff);
my_syslog(MS_TFTP | LOG_INFO, endcon ? _("failed sending %s to %s") : _("sent %s to %s"), daemon->namebuff, daemon->addrbuff); my_syslog(MS_TFTP | LOG_INFO, endcon ? _("failed sending %s to %s") : _("sent %s to %s"), daemon->namebuff, daemon->addrbuff);
/* unlink */ /* unlink */
*up = tmp; *up = tmp;
if (endcon) if (endcon)
free_transfer(transfer); free_transfer(transfer);
else else
{ {
/* put on queue to be sent to script and deleted */ /* put on queue to be sent to script and deleted */
transfer->next = daemon->tftp_done_trans; transfer->next = daemon->tftp_done_trans;
daemon->tftp_done_trans = transfer; daemon->tftp_done_trans = transfer;
} }
continue; continue;
} }
} }
up = &transfer->next; up = &transfer->next;
} }
} }
/* packet in daemon->packet as this is called. */
static void handle_tftp(time_t now, struct tftp_transfer *transfer, ssize_t len)
{
struct ack {
unsigned short op, block;
} *mess = (struct ack *)daemon->packet;
if (len >= (ssize_t)sizeof(struct ack))
{
if (ntohs(mess->op) == OP_ACK && ntohs(mess->block) == (unsigned short)tra
nsfer->block)
{
/* Got ack, ensure we take the (re)transmit path */
transfer->timeout = now;
transfer->backoff = 0;
if (transfer->block++ != 0)
transfer->offset += transfer->blocksize - transfer->expansion;
}
else if (ntohs(mess->op) == OP_ERR)
{
char *p = daemon->packet + sizeof(struct ack);
char *end = daemon->packet + len;
char *err = next(&p, end);
prettyprint_addr(&transfer->peer, daemon->addrbuff);
/* Sanitise error message */
if (!err)
err = "";
else
sanitise(err);
my_syslog(MS_TFTP | LOG_ERR, _("error %d %s received from %s"),
(int)ntohs(mess->block), err,
daemon->addrbuff);
/* Got err, ensure we take abort */
transfer->timeout = now;
transfer->backoff = 100;
}
}
}
static void free_transfer(struct tftp_transfer *transfer) static void free_transfer(struct tftp_transfer *transfer)
{ {
close(transfer->sockfd); if (!option_bool(OPT_SINGLE_PORT))
close(transfer->sockfd);
if (transfer->file && (--transfer->file->refcount) == 0) if (transfer->file && (--transfer->file->refcount) == 0)
{ {
close(transfer->file->fd); close(transfer->file->fd);
free(transfer->file); free(transfer->file);
} }
free(transfer); free(transfer);
} }
static char *next(char **p, char *end) static char *next(char **p, char *end)
{ {
char *ret = *p; char *ret = *p;
size_t len; size_t len;
if (*(end-1) != 0 || if (*(end-1) != 0 ||
*p == end || *p == end ||
 End of changes. 38 change blocks. 
86 lines changed or deleted 126 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)