Ok. I added a lot of security fixes added a feature to put in a custom
floating point timeout as an argument and got rid of the 8 mirror limit. It
puts in all the mirrors that didn't either exceed the timeout period or
have a download error. It should be safe to run as root I guess. There is
nothing that can be done to make it core dump. The only thing, I suspect,
that can go wrong is a man in the middle attack downloading ftp.html. Is
there even a hash value for ftp.html ?

-Luke

On Thu, Jan 21, 2016 at 1:18 AM, Luke Small <[email protected]> wrote:

> The real reason I wrote this is to have an automated way to set up the
> pkg_add mirrors especially for folks that don't care to set them up
> manually (Afterall, that's what computers are for!). Before I wrote this, I
> had a PKG_PATH mirror go down and I didn't know what was going on. At least
> this could get some failover that would work for everyone running the
> release or older at least. I put in a minor edit that kills ftp and sed if
> the buffer gets too full, as well as exiting.
>
>>
>>
>>
>>> > > I have a 500 line program I wrote that reads openbsd.org.ftp.html and
>>>
>>> Here's a simple alternative that will often be good enough.
>>>
>>> ftp -o- -V http://www.openbsd.org/cgi-bin/ftplist.cgi |
>>>     sed -e 's, .*,/%m/,' -e 's,^,pkgpath = ,' -e q
>>>
>>> The C program is too trusting with its fixed-size buffers and unchecked
>>> mallocs etc, it's not something to run as root as-is.
>>>
>>
>>
>
/*
 * Copyright (c) 2016 Luke N. Small
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
 

/* Special thanks to Dan Mclaughlin for the ftp to sed idea
 * 
 * ftp -o - http://www.openbsd.org/ftp.html | \
 * sed -n \
 *  -e 's:</a>$::' \
 * 	-e 's:	<strong>\([^<]*\)<.*:\1:p' \
 * 	-e 's:^\(	[hfr].*\):\1:p'
 */
 
 
#define EVENT_NOPOLL
#define EVENT_NOSELECT

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <err.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/event.h>
#include <signal.h>
#include <string.h>

struct mirror_st
{
	char * countryTitle;
	char * mirror;
	char * installPath;
	double diff;
	struct mirror_st * next;
};

int ftp_cmp (const void * a, const void * b)
{
	struct mirror_st ** one = (struct mirror_st **)a;
	struct mirror_st ** two = (struct mirror_st **)b;

	if( (*one)->diff < (*two)->diff )
		return -1;
	if( (*one)->diff > (*two)->diff )
		return 1;
	return 0;
}

int country_cmp (const void * a, const void * b)
{
	struct mirror_st ** one = (struct mirror_st **)a;
	struct mirror_st ** two = (struct mirror_st **)b;
	
	// list the USA mirrors first, it will subsort correctly
	int8_t temp = !strncmp("USA", (*one)->countryTitle, 3);
	if(temp != !strncmp("USA", (*two)->countryTitle, 3))
	{
		if(temp)
			return -1;
		return 1;
	}

	return strcmp( (*one)->countryTitle, (*two)->countryTitle ) ;
}


double getTimeDiff(struct timeval a, struct timeval b)
{
    long sec;
    long usec;
    sec = b.tv_sec - a.tv_sec;
    usec = b.tv_usec - a.tv_usec;
    if (usec < 0)
    {
		--sec;
		usec += 1000000;
    }
    return sec + ((double)usec / 1000000.0);
}

// Can take one argument which sets a positive floating point timeout
int main(int argc, char *argv[])
{
	pid_t ftpPid, sedPid;
	int ftpToSed[2];
	int sedToParent[2];
	int unameToParent[2];
	char unameR[5], unameM[20], Char;
	int i, c;
	register int position, num;

	FILE *input;

	if(argc > 1)
	{
		c = -1;
		i = 0;
		while((Char = argv[1][++c]) != '\0')
		{
			if(Char == '.')
				++i;
			
			if( ((Char < '0' || Char > '9') && Char != '.') || i > 1 )
			{
				if(Char == '-')
					errx(1, "No negative numbers.");
				errx(1, "Incorrect floating point format.");
			}
		}
		strtod(argv[1], NULL);
		if(errno == ERANGE)
			err(1, NULL);
	}


	pipe(unameToParent);
	

	// "uname -rm" returns version and architecture like: "5.8 amd64" to standard out
	
	if(fork() == (pid_t) 0)
	{							/* uname child */
		close(unameToParent[0]);
		dup2(unameToParent[1], STDOUT_FILENO);         /*attaching to pipe(s)*/
		execl("/usr/bin/uname","/usr/bin/uname", "-rm", NULL);
        errx(1, "execl() failed.");
	}
	
	close(unameToParent[1]);

	input = fdopen (unameToParent[0], "r");

	num = 0;
	position = -1;
	while ((c = getc(input)) != EOF)
	{
		if(num == 0)
		{
			if(++position >= 5)
				errx(1, "unameR[] got too long!");
			if(c != ' ')
				unameR[position] = c;
			else
			{
				unameR[position] = '\0';
				num = 1;
				position = -1;
			}
		}
		else
		{
			if(++position >= 20)
				errx(1, "unameM[] got too long!");
			if(c != '\n')
				unameM[position] = c;
			else
			{
				unameM[position] = '\0';
				break;
			}
		}
	}
	fclose (input);
	close(unameToParent[0]);





	pipe(ftpToSed);         /*make pipes*/

	struct kevent ke[2];

	int kq = kqueue();
	if (kq == -1)
		errx(1, "kq!");

	int kqProc = kqueue();
	if (kqProc == -1)
		errx(1, "kqProc!");
		
		
	ftpPid = fork();
	if(ftpPid == (pid_t) 0)
	{							/*ftp child*/
		close(ftpToSed[0]);
		dup2(ftpToSed[1], STDOUT_FILENO);         /*attaching to pipe(s)*/
        execl("/usr/bin/ftp","ftp", "-Vo", "-", "http://www.openbsd.org/ftp.html";, NULL);
        errx(1, "execl() failed.");
	}
	EV_SET(ke, ftpPid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, &ftpPid);
	if (kevent(kqProc, ke, 1, NULL, 0, NULL) == -1)
		errx(1, "kevent register fail.");

	close(ftpToSed[1]);

	pipe(sedToParent);
	

	sedPid = fork();
	if(sedPid == (pid_t) 0)
	{							/* sed child */
		close(sedToParent[0]);
		dup2(ftpToSed[0], STDIN_FILENO);         /*attaching to pipe(s)*/
		dup2(sedToParent[1], STDOUT_FILENO);
		execl("/usr/bin/sed","sed","-n",
								   "-e", "s:</a>$::",
								   "-e", "s:\t<strong>\\([^<]*\\)<.*:\\1:p",
								   "-e", "s:^\\(\t[hfr].*\\):\\1:p", NULL);
		kill(ftpPid, SIGKILL);
        errx(1, "execl() failed.");
	}
	EV_SET(ke, sedPid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, &sedPid);
	if (kevent(kqProc, ke, 1, NULL, 0, NULL) == -1)
		errx(1, "kevent register fail.");
	

	close(ftpToSed[0]);         /*close pipe(s) child attached to*/
	close(sedToParent[1]);

	EV_SET(ke, sedToParent[0], EVFILT_READ, EV_ADD | EV_ONESHOT, 0, 0, NULL);
	if (kevent(kq, ke, 1, NULL, 0, NULL) == -1)
		errx(1, "kevent register fail.");

	struct timespec timeout;
	
	if(argc == 1)
	{
		timeout.tv_sec = 7;
		timeout.tv_nsec = 0;
	}
	else
	{
		timeout.tv_sec = (int)strtod(argv[1], NULL);
		timeout.tv_nsec = (int)(   (strtod(argv[1], NULL) - (double)timeout.tv_sec) * 1000000000  );
	}

	i = kevent(kq, NULL, 0, ke, 1, &timeout);
	if (i == -1)
		errx(1, "kevent, timeout may be too large");
		
	if(i == 0)
	{
		close(sedToParent[0]);
		printf("timed out.\n");
		kill(ftpPid, SIGKILL);
		kill(sedPid, SIGKILL);
		return -1;
	}
    
	input = fdopen (sedToParent[0], "r");
	
	char line[300];
	num = 0;
	position = 0;
	struct mirror_st *start, *end, *mTemp1, *mTemp2, *mTemp3;
	start = end = malloc(sizeof(struct mirror_st));
	if(start == NULL)
		errx(1, "malloc failed.");
	start->next = NULL;

	while((c = getc (input)) != EOF)
	{
		if(position >= 300)
		{
			kill(ftpPid, SIGKILL);
			kill(sedPid, SIGKILL);
			errx(1, "line[] got too long!");
		}
		if(num == 0)
		{
			if( c != '\n' )
				line[position++] = c;
			else
			{
				// there is a space before the newline that is eliminated
				line[position++] = '\0';
				
				end->countryTitle = malloc(position);
				if(end->countryTitle == NULL)
				{
					kill(ftpPid, SIGKILL);
					kill(sedPid, SIGKILL);
					errx(1, "malloc failed.");
				}

				strlcpy(end->countryTitle, line, position);
				
				position = 0;
				num = 1;
			}		
		}
		else
		{
			if(position == 0)
			{
				if( (c != 'h') && (c != 'f') && (c != 'r') )
					continue;
				else if(c == 'r')
					break;
				else if(c == 'f')
				{
					// changes ftp listings to http. ftp.html says they can be either
					line[position++] = 'h';
					line[position++] = 't';
					continue;
				}
			}
			if( c != '\n' )
				line[position++] = c;
			else
			{
				line[position++] = '\0';

				position += num = strlen(unameR) + 10 + strlen(unameM) + 7;
				end->mirror = malloc(position);
				if(end->mirror == NULL)
				{
					kill(ftpPid, SIGKILL);
					kill(sedPid, SIGKILL);
					errx(1, "malloc failed.");
				}
				c =  strlcpy(end->mirror,             line, position);
				c += strlcpy(end->mirror + c,       unameR, position - c);
				c += strlcpy(end->mirror + c, "/packages/", position - c);
				c += strlcpy(end->mirror + c,       unameM, position - c);
				     strlcpy(end->mirror + c,    "/SHA256", position - c);

				position += 15 - num;				
				end->installPath = malloc(position);
				if(end->installPath == NULL)
				{
					kill(ftpPid, SIGKILL);
					kill(sedPid, SIGKILL);
					errx(1, "malloc failed.");
				}
				c = strlcpy(end->installPath,                  line, position);
				    strlcpy(end->installPath + c, "%c/packages/%a/", position - c);
				
				
				end = end->next = malloc(sizeof(struct mirror_st));
				if(end == NULL)
				{
					kill(ftpPid, SIGKILL);
					kill(sedPid, SIGKILL);
					errx(1, "malloc failed.");
				}
				end->next = NULL;
				
				position = 0;
				num = 0;
			}			
		}
	}

	fclose (input);
	
	// this is to prevent possible segfault in the next sequence.
	if(start == end)
	{
		kill(ftpPid, SIGKILL);
		kill(sedPid, SIGKILL);
		errx(1, "Too few entries found.");
	}
	
	mTemp1 = start;
	while(mTemp1->next != end)
		mTemp1 = mTemp1->next;
	free(end->countryTitle);
	free(end);
	end = mTemp1;
	end->next = NULL;
	
	// Eliminate redundant mirrors (no longer need the 'end' value)
	mTemp1 = start;
	while(mTemp1->next != NULL)
	{
		mTemp2 = mTemp1;
		do
		{
			if(!strcmp(mTemp1->mirror, mTemp2->next->mirror))
			{
				mTemp3 = mTemp2->next;
				mTemp2->next = mTemp3->next;
				free(mTemp3->countryTitle);
				free(mTemp3->installPath);
				free(mTemp3->mirror);
				free(mTemp3);
			}
			mTemp2 = mTemp2->next;
			if(mTemp2 == NULL)
				break;
		} while( mTemp2->next != NULL );
		mTemp1 = mTemp1->next;
	}
	
	int mirrorNum = 0;
	mTemp1 = start;
	while(mTemp1 != NULL)
	{
		++mirrorNum;
		mTemp1 = mTemp1->next;
	}

	struct mirror_st ** Array;

	if( (Array = calloc(mirrorNum, sizeof(struct mirror_st *))) == NULL)
		errx(1, "calloc failed.");
	mTemp1 = start;
	for(c = 0; c < mirrorNum; ++c)
	{
		Array[c] = mTemp1;
		
		mTemp1 = mTemp1->next;
	}
	
	qsort(Array, mirrorNum, sizeof(struct mirror_st *), country_cmp);

	close(sedToParent[0]);
    	
	i = kevent(kqProc, NULL, 0, ke, 2, &timeout);
	if (i == -1)
		errx(1, "kevent");
		
	if(i == 0)
	{
		kill(ftpPid, SIGKILL);
		kill(sedPid, SIGKILL);
		errx(1, "timed out fetching openbsd.org/ftp.html.");
	}
	else if(i == 1)
	{
		if( ( (int *)ke->udata ) == &sedPid)
			kill(ftpPid, SIGKILL);
		else
			kill(sedPid, SIGKILL);
		errx(1, "timed out fetching openbsd.org/ftp.html.");
	}
	
		
	close(kq);
	close(kqProc);
	
	kqProc = kqueue();
	if (kqProc == -1)
		errx(1, "kqProc!");

	int pid;
	
	for(c = 0; c < mirrorNum; ++c)
	{
		mTemp1 = Array[c];

		printf("\n%d : %s  :  %s\n", (mirrorNum - c) - 1, mTemp1->countryTitle, mTemp1->mirror);

		pid = fork();
		if(pid == (pid_t) 0)
		{							/* ping child */
			execl("/usr/bin/ftp", "ftp", "-Vmo", "/dev/null", mTemp1->mirror, NULL);
			errx(1, "execl() failed.");
		}
		EV_SET(ke, pid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, NULL);
		if (kevent(kqProc, ke, 1, NULL, 0, NULL) == -1)
			errx(1, "kevent register fail.");

		mTemp1->diff = 0;
		struct timeval tv_start, tv_end;
		gettimeofday(&tv_start, NULL);

		skip:

		i = kevent(kqProc, NULL, 0, ke, 1, &timeout);
		if (i == -1)
			errx(1, "kevent");
		if(i == 0)
		{
			printf("\n");
			kill(pid, SIGKILL);
			mTemp1->diff = timeout.tv_sec + (double)timeout.tv_nsec / 1000000000.0;
			
			// Loop until ftp() is dead.
			goto skip;
		}

		if( ke->data == 0 )
		{
			gettimeofday(&tv_end, NULL);

			mTemp1->diff = getTimeDiff(tv_start, tv_end);
		}
		else if( mTemp1->diff == 0 )
			mTemp1->diff  =  timeout.tv_sec + (double)timeout.tv_nsec / 1000000000.0 + 1;
	}

	qsort(Array, mirrorNum, sizeof(struct mirror_st *), ftp_cmp);

	for(c = mirrorNum - 1; c >= 0; --c)
	{
		printf("%d : %s:\n\t%s : ", c, Array[c]->countryTitle, Array[c]->installPath);
		
		if(Array[c]->diff < timeout.tv_sec + (double)timeout.tv_nsec / 1000000000.0)
			printf("%f\n\n", Array[c]->diff);
		else if(Array[c]->diff == timeout.tv_sec + (double)timeout.tv_nsec / 1000000000.0)
			printf("Timeout\n\n");
		else
			printf("Download Error\n\n");
	}
	
	if(Array[0]->diff >= timeout.tv_sec + (double)timeout.tv_nsec / 1000000000.0)
		errx(1, "No mirrors found within timeout period.");

	char *buf;
	int lines;
	int totalLength = 0;
	for(position = 0; position < mirrorNum; ++position)
		totalLength += strlen("installpath += \n") + strlen(Array[position]->installPath);

	if( (input = fopen("/etc/pkg.conf", "r")) == NULL )
	{
		buf = (char*)malloc(totalLength);
		if(buf == NULL)
			errx(1, "malloc failed.");
		c  = strlcpy(buf,          "installpath = ", totalLength);
		c += strlcpy(buf + c, Array[0]->installPath, totalLength - c);

		position = 1;
		while(position < mirrorNum)
		{
			// Eliminates dowload error mirrors
			if(Array[position]->diff >= timeout.tv_sec + (double)timeout.tv_nsec / 1000000000.0)
				break;
			c += strlcpy(buf + c,            "\ninstallpath += ", totalLength - c);
			c += strlcpy(buf + c, Array[position++]->installPath, totalLength - c);
		}		
	}
	else
	{
		fseek(input, 0, SEEK_END);	
		num = ftell(input);
		fseek(input, 0, SEEK_SET);	
		lines = 0;
		buf = (char*)malloc(num + totalLength + 11);
		if(buf == NULL)
			errx(1, "malloc failed.");
		fread(buf, 1, num, input);
		fclose(input);
		for(c = 0; c < num; ++c)
		{
			if(buf[c] == '\n')
				++lines;
		}

		position = 0;
		int copy = 0;
		for(c = 0; c < num; ++c)
		{
			if(buf[c] == '\n')
			{
				--lines;
				position = 0;
			}
			else if(position++ == 0)
			{
				if( !strncmp(buf + c, "installpath", 11) )
				{
					if(lines)
					{
						while(buf[++c] != '\n');
						--lines;
						position = 0;
						continue;
					}
					else
						break;
				}
			}
			buf[copy++] = buf[c];
		}


		totalLength += num;
		c = copy + strlcpy(buf + copy,        "installpath = ", totalLength - copy);
		c +=       strlcpy(buf + c,      Array[0]->installPath, totalLength - c);

		position = 1;
		while(position < mirrorNum)
		{
			// Eliminates dowload error mirrors
			if(Array[position]->diff >= timeout.tv_sec + (double)timeout.tv_nsec / 1000000000.0)
				break;
			c += strlcpy(buf + c,          "\ninstallpath += ", totalLength - c);
			c += strlcpy(buf + c, Array[position++]->installPath, totalLength - c);
		}
	}
	
	if(argc == 1)
		printf("\n\nA floating point argument is available to change the timeout period.\n\n");
	else
		printf("\n\n");

	input = fopen("/etc/pkg.conf", "w");
	if(input != NULL)
	{
		fwrite(buf, 1, c, input);
		fclose(input);
		printf("Edit out all PKG_PATH environment exports and run \"unset PKG_PATH\".\n\n");
		printf("/etc/pkg.conf:\n%s\n", buf);
	}
	else
	{
		printf("This could have been the contents of /etc/pkg.conf (run as superuser):\n");
		printf("%s\n", buf);
	}

	return 0;
}

Reply via email to