blob: 3c4a1f1df400ced96568032ae463f6d41bba3b15 [file] [log] [blame]
Simon Kelleyfd9fa482004-10-21 20:24:00 +01001#!/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}