1#!/usr/bin/perl
2# dynamic-dnsmasq.pl - update dnsmasq's internal dns entries dynamically
3# Copyright (C) 2004  Peter Willis
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18#
19# the purpose of this script is to be able to update dnsmasq's dns
20# records from a remote dynamic dns client.
21#
22# basic use of this script:
23# dynamic-dnsmasq.pl add testaccount 1234 testaccount.mydomain.com
24# dynamic-dnsmasq.pl listen &
25#
26# this script tries to emulate DynDNS.org's dynamic dns service, so
27# technically you should be able to use any DynDNS.org client to
28# update the records here. tested and confirmed to work with ddnsu
29# 1.3.1. just point the client's host to the IP of this machine,
30# port 9020, and include the hostname, user and pass, and it should
31# work.
32#
33# make sure "addn-hosts=/etc/dyndns-hosts" is in your /etc/dnsmasq.conf
34# file and "nopoll" is commented out.
35
36use strict;
37use IO::Socket;
38use MIME::Base64;
39use DB_File;
40use Fcntl;
41
42my $accountdb = "accounts.db";
43my $recordfile = "/etc/dyndns-hosts";
44my $dnsmasqpidfile = "/var/run/dnsmasq.pid"; # if this doesn't exist, will look for process in /proc
45my $listenaddress = "0.0.0.0";
46my $listenport = 9020;
47
48# no editing past this point should be necessary
49
50if ( @ARGV < 1 ) {
51	die "Usage: $0 ADD|DEL|LISTUSERS|WRITEHOSTSFILE|LISTEN\n";
52} elsif ( lc $ARGV[0] eq "add" ) {
53	die "Usage: $0 ADD USER PASS HOSTNAME\n" unless @ARGV == 4;
54	add_acct($ARGV[1], $ARGV[2], $ARGV[3]);
55} elsif ( lc $ARGV[0] eq "del" ) {
56	die "Usage: $0 DEL USER\n" unless @ARGV == 2;
57	print "Are you sure you want to delete user \"$ARGV[1]\"? [N/y] ";
58	my $resp = <STDIN>;
59	chomp $resp;
60	if ( lc substr($resp,0,1) eq "y" ) {
61		del_acct($ARGV[1]);
62	}
63} elsif ( lc $ARGV[0] eq "listusers" or lc $ARGV[0] eq "writehostsfile" ) {
64	my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH;
65	my $fh;
66	if ( lc $ARGV[0] eq "writehostsfile" ) {
67        	open($fh, ">$recordfile") || die "Couldn't open recordfile \"$recordfile\": $!\n";
68	       	flock($fh, 2);
69	       	seek($fh, 0, 0);
70	       	truncate($fh, 0);
71        }
72	while ( my ($key, $val) = each %h ) {
73		my ($pass, $domain, $ip) = split("\t",$val);
74		if ( lc $ARGV[0] eq "listusers" ) {
75			print "user $key, hostname $domain, ip $ip\n";
76		} else {
77			if ( defined $ip ) {
78				print $fh "$ip\t$domain\n";
79			}
80		}
81	}
82	if ( lc $ARGV[0] eq "writehostsfile" ) {
83		flock($fh, 8);
84		close($fh);
85		dnsmasq_rescan_configs();
86	}
87	undef $X;
88	untie %h;
89} elsif ( lc $ARGV[0] eq "listen" ) {
90	listen_for_updates();
91}
92
93sub listen_for_updates {
94	my $sock = IO::Socket::INET->new(Listen    => 5,
95		LocalAddr => $listenaddress, LocalPort => $listenport,
96		Proto     => 'tcp', ReuseAddr => 1,
97		MultiHomed => 1) || die "Could not open listening socket: $!\n";
98	$SIG{'CHLD'} = 'IGNORE';
99	while ( my $client = $sock->accept() ) {
100		my $p = fork();
101		if ( $p != 0 ) {
102			next;
103		}
104		$SIG{'CHLD'} = 'DEFAULT';
105		my @headers;
106		my %cgi;
107		while ( <$client> ) {
108			s/(\r|\n)//g;
109			last if $_ eq "";
110			push @headers, $_;
111		}
112		foreach my $header (@headers) {
113			if ( $header =~ /^GET \/nic\/update\?([^\s].+) HTTP\/1\.[01]$/ ) {
114				foreach my $element (split('&', $1)) {
115					$cgi{(split '=', $element)[0]} = (split '=', $element)[1];
116				}
117			} elsif ( $header =~ /^Authorization: basic (.+)$/ ) {
118				unless ( defined $cgi{'hostname'} ) {
119					print_http_response($client, undef, "badsys");
120					exit(1);
121				}
122				if ( !exists $cgi{'myip'} ) {
123					$cgi{'myip'} = $client->peerhost();
124				}
125				my ($user,$pass) = split ":", MIME::Base64::decode($1);
126				if ( authorize($user, $pass, $cgi{'hostname'}, $cgi{'myip'}) == 0 ) {
127					print_http_response($client, $cgi{'myip'}, "good");
128					update_dns(\%cgi);
129				} else {
130					print_http_response($client, undef, "badauth");
131					exit(1);
132				}
133				last;
134			}
135		}
136		exit(0);
137	}
138	return(0);
139}
140
141sub add_acct {
142	my ($user, $pass, $hostname) = @_;
143	my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH;
144	$X->put($user, join("\t", ($pass, $hostname)));
145	undef $X;
146	untie %h;
147}
148
149sub del_acct {
150        my ($user, $pass, $hostname) = @_;
151        my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH;
152        $X->del($user);
153        undef $X;
154        untie %h;
155}
156
157
158sub authorize {
159	my $user = shift;
160	my $pass = shift;
161	my $hostname = shift;
162	my $ip = shift;;
163	my $X = tie my %h, "DB_File", $accountdb, O_RDWR|O_CREAT, 0600, $DB_HASH;
164	my ($spass, $shost) = split("\t", $h{$user});
165	if ( defined $h{$user} and ($spass eq $pass) and ($shost eq $hostname) ) {
166		$X->put($user, join("\t", $spass, $shost, $ip));
167		undef $X;
168		untie %h;
169		return(0);
170	}
171	undef $X;
172	untie %h;
173	return(1);
174}
175
176sub print_http_response {
177	my $sock = shift;
178	my $ip = shift;
179	my $response = shift;
180	print $sock "HTTP/1.0 200 OK\n";
181	my @tmp = split /\s+/, scalar gmtime();
182	print $sock "Date: $tmp[0], $tmp[2] $tmp[1] $tmp[4] $tmp[3] GMT\n";
183	print $sock "Server: Peter's Fake DynDNS.org Server/1.0\n";
184	print $sock "Content-Type: text/plain; charset=ISO-8859-1\n";
185	print $sock "Connection: close\n";
186	print $sock "Transfer-Encoding: chunked\n";
187	print $sock "\n";
188	#print $sock "12\n"; # this was part of the dyndns response but i'm not sure what it is
189	print $sock "$response", defined($ip)? " $ip" : "" . "\n";
190}
191
192sub update_dns {
193	my $hashref = shift;
194	my @records;
195	my $found = 0;
196	# update the addn-hosts file
197	open(FILE, "+<$recordfile") || die "Couldn't open recordfile \"$recordfile\": $!\n";
198	flock(FILE, 2);
199	while ( <FILE> ) {
200		if ( /^(\d+\.\d+\.\d+\.\d+)\s+$$hashref{'hostname'}\n$/si ) {
201			if ( $1 ne $$hashref{'myip'} ) {
202				push @records, "$$hashref{'myip'}\t$$hashref{'hostname'}\n";
203				$found = 1;
204			}
205		} else {
206			push @records, $_;
207		}
208	}
209	unless ( $found ) {
210		push @records, "$$hashref{'myip'}\t$$hashref{'hostname'}\n";
211	}
212	sysseek(FILE, 0, 0);
213	truncate(FILE, 0);
214	syswrite(FILE, join("", @records));
215	flock(FILE, 8);
216	close(FILE);
217	dnsmasq_rescan_configs();
218	return(0);
219}
220
221sub dnsmasq_rescan_configs {
222	# send the HUP signal to dnsmasq
223	if ( -r $dnsmasqpidfile ) {
224		open(PID,"<$dnsmasqpidfile") || die "Could not open PID file \"$dnsmasqpidfile\": $!\n";
225		my $pid = <PID>;
226		close(PID);
227		chomp $pid;
228		if ( kill(0, $pid) ) {
229			kill(1, $pid);
230		} else {
231			goto LOOKFORDNSMASQ;
232		}
233	} else {
234		LOOKFORDNSMASQ:
235		opendir(DIR,"/proc") || die "Couldn't opendir /proc: $!\n";
236		my @dirs = grep(/^\d+$/, readdir(DIR));
237		closedir(DIR);
238		foreach my $process (@dirs) {
239			if ( open(FILE,"</proc/$process/cmdline") ) {
240				my $cmdline = <FILE>;
241				close(FILE);
242				if ( (split(/\0/,$cmdline))[0] =~ /dnsmasq/ ) {
243					kill(1, $process);
244				}
245			}
246		}
247	}
248	return(0);
249}
250