source: trunk/cgi-bin/search-rpc.cgi@ 949

Last change on this file since 949 was 946, checked in by Kris Deugau, 14 months ago

/trunk

cgi-bin/search-rpc.cgi:

  • Fix trivial typo refreshing memory how to use the search RPC script
  • Update copyright and bring license comment into line
  • Property svn:executable set to *
File size: 14.4 KB
RevLine 
[903]1#!/usr/bin/perl
[946]2# ipdb/cgi-bin/search-rpc.cgi
[903]3# XMLRPC interface to IPDB search
[946]4# Copyright (C) 2017,2019,2023 - Kris Deugau <kdeugau@deepnet.cx>
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18##
[903]19
20use strict;
21use warnings;
22
23use DBI;
24use NetAddr::IP;
25use FCGI;
26use Frontier::Responder;
27
28use Sys::Syslog;
29
30# don't remove! required for GNU/FHS-ish install from tarball
31##uselib##
32
[906]33# push "the directory the script is in" into @INC
34use FindBin;
35use lib "$FindBin::RealBin/";
36
[903]37use MyIPDB;
[907]38use CustIDCK;
[903]39
40openlog "IPDB-search-rpc","pid","$IPDB::syslog_facility";
41
42##fixme: username source? can we leverage some other auth method?
43# we don't care except for logging here, and Frontier::Client needs
44# a patch that's not well-distributed to use HTTP AUTH.
45
46# Collect the username from HTTP auth. If undefined, we're in
47# a test environment, or called without a username.
48my $authuser;
49if (!defined($ENV{'REMOTE_USER'})) {
50 $authuser = '__temptest';
51} else {
52 $authuser = $ENV{'REMOTE_USER'};
53}
54
55# Why not a global DB handle? (And a global statement handle, as well...)
56# Use the connectDB function, otherwise we end up confusing ourselves
57my $ip_dbh;
58my $sth;
59my $errstr;
60($ip_dbh,$errstr) = connectDB_My;
61initIPDBGlobals($ip_dbh);
62
63my $methods = {
64 'ipdb.search' => \&rpc_search,
65};
66
67my $reqcnt = 0;
68
69my $req = FCGI::Request();
70
71# main FCGI loop.
72while ($req->Accept() >= 0) {
73 # done here to a) prevent $ENV{'REMOTE_ADDR'} from being empty and b) to collect
74 # the right user for the individual call (since we may be running with FCGI)
75 syslog "debug", "$authuser active, $ENV{'REMOTE_ADDR'}";
76
77 # don't *think* we need any of these...
78 # %disp_alloctypes, %def_custids, %list_alloctypes
79 # @citylist, @poplist
80 # @masterblocks, %allocated, %free, %bigfree, %routed (removed in /trunk)
81 # %IPDBacl
82 #initIPDBGlobals($ip_dbh);
83
84 my $res = Frontier::Responder->new(
85 methods => $methods
86 );
87
88 # "Can't do that" errors
89 if (!$ip_dbh) {
90 print "Content-type: text/xml\n\n".$res->{_decode}->encode_fault(5, $DBI::errstr);
91 } else {
92 print $res->answer;
93 }
94 last if $reqcnt++ > $IPDB::maxfcgi;
95} # while FCGI::accept
96
97exit 0;
98
99
100##
101## Private subs
102##
103
104# Check RPC ACL
105sub _aclcheck {
106 my $subsys = shift;
107 return 1 if grep /$ENV{REMOTE_ADDR}/, @{$IPDB::rpcacl{$subsys}};
108 warn "$subsys/$ENV{REMOTE_ADDR} not in ACL\n"; # a bit of logging
109 return 0;
110}
111
112sub _commoncheck {
113 my $argref = shift;
114 my $needslog = shift;
115
116 die "Missing remote system name\n" if !$argref->{rpcsystem};
117 die "Access denied\n" if !_aclcheck($argref->{rpcsystem});
118 if ($needslog) {
119 die "Missing remote username\n" if !$argref->{rpcuser};
120 }
121}
122
123# stripped-down copy from from main.cgi. should probably be moved to IPDB.pm
124sub _validateInput {
125 my $argref = shift;
126
127 if (!$argref->{block}) {
128 $argref->{block} = $argref->{cidr} if $argref->{cidr};
129 die "Block/IP is required\n" if !$argref->{block};
130 }
131
132 # Alloctype check.
133 chomp $argref->{type};
134
135 die "Invalid allocation type\n" if (!grep /$argref->{type}/, keys %disp_alloctypes);
136
137 # Arguably not quite correct, as the custID won't be checked for
138 # validity if there's a default on the type.
139 if ($def_custids{$argref->{type}} eq '') {
140 # Types without a default custID must have one passed in
141 die "Customer ID is required\n" if !$argref->{custid};
142 # Crosscheck with billing.
143 my $status = CustIDCK->custid_exist($argref->{custid});
144 die "Error verifying customer ID: $CustIDCK::ErrMsg\n" if $CustIDCK::Error;
145 die "Customer ID not valid\n" if !$status;
146 } else {
147 # Types that have a default will use it unless one is specified.
148 if ((!$argref->{custid}) || ($argref->{custid} ne 'STAFF')) {
149 $argref->{custid} = $def_custids{$argref->{type}};
150 }
151 }
152} # end validateInput()
153
154
155##
156## RPC method subs
157##
158
159sub rpc_search {
160 my %args = @_;
161
[911]162 _commoncheck(\%args);
[903]163
164 my @fields;
165 my @vals;
166 my @matchtypes;
167
[908]168 my %mt = (
169 EXACT => '=',
[905]170 EQUAL => '=',
[908]171 NOT => '!~', # text only?
[905]172 # CIDR options
173 MASK => 'MASK',
174 WITHIN => '<<=',
175 CONTAINS => '>>=',
[903]176 );
177
178 if ($args{type}) {
[909]179 # assume alloctype class if we only get one letter
180 $args{type} = "_$args{type}" if $args{type} =~ /^.$/;
[908]181 my $notflag = '';
182 if ($args{type} =~ /^NOT:/) {
183 $args{type} =~ s/^NOT://;
184 $notflag = 'NOT ';
185 }
186 if ($args{type} =~ /\./) {
187 $args{type} =~ s/\./_/;
188 push @matchtypes, $notflag.'LIKE';
189 } else {
190 push @matchtypes, ($notflag ? '<>' : '=');
191 }
[903]192 push @fields, 's.type';
193 push @vals, $args{type};
194 }
[905]195
196 ## CIDR query options.
[903]197 if ($args{cidr}) {
[905]198 $args{cidr} =~ s/^\s*(.+)\s*$/$1/g;
199 # strip matching type substring, if any - only applies to full-CIDR
200 my ($mnote) = $args{cidr} =~ /^(\w+):/;
201 $args{cidr} =~ s/^$mnote:// if $mnote;
202
203 if ($args{cidr} eq '') { # We has a blank CIDR. Ignore it.
204 } elsif ($args{cidr} =~ /\//) {
205 my ($net,$maskbits) = split /\//, $args{cidr};
206 if ($args{cidr} =~ /^(\d{1,3}\.){3}\d{1,3}\/\d{2}$/) {
207 # Full CIDR match.
208 push @fields, 's.cidr';
209 push @vals, $args{cidr};
210 if ($mnote =~ /(EQUAL|EXACT|CONTAINS|WITHIN)/) {
211 push @matchtypes, $mt{$1};
212 } else { # default to exact match
213 push @matchtypes, '=';
214 }
215 } elsif ($args{cidr} =~ /^(\d{1,3}\.){2}\d{1,3}\/\d{2}$/) {
216 # Partial match; beginning of subnet and maskbits are provided
217 # Show any blocks with the leading octet(s) and that masklength
218 # eg 192.168.179/26 should show all /26 subnets in 192.168.179
219 # Need some more magic for bare /nn searches:
220 push @fields, 's.cidr','masklen(s.cidr)';
221 push @vals, "$net.0/24", $maskbits;
222 push @matchtypes, '<<=','=';
223 }
224 } elsif ($args{cidr} =~ /^(\d{1,3}\.){3}\d{1,3}$/) {
225 # Specific IP address match. Will show the parent chain down to the final allocation.
226 push @fields, 's.cidr';
227 push @vals, $args{cidr};
228 push @matchtypes, '>>=';
229 } elsif ($args{cidr} =~ /^\d{1,3}(\.(\d{1,3}(\.(\d{1,3}\.?)?)?)?)?$/) {
230 # 1, 2, or 3 leading octets in CIDR
231 push @fields, 'text(s.cidr)';
232 push @vals, "$args{cidr}\%";
233 push @matchtypes, 'LIKE'; # hmm
234 } else {
235 # do nothing.
236 ##fixme we'll ignore this to clear out the references to legacy code.
237 } # done with CIDR query options.
238
239 } # args{cidr}
240
[925]241 foreach my $sfield (qw(custid description notes city) ) {
242 if ($args{$sfield}) {
243 push @fields, "s.$sfield";
244 if ($args{$sfield} =~ /^(EXACT|NOT):/) {
[903]245 push @matchtypes, $mt{$1};
[925]246 $args{$sfield} =~ s/^$1://;
[903]247 } else {
248 push @matchtypes, '~*';
249 }
[925]250 push @vals, $args{$sfield};
[903]251 }
252 }
253
[925]254 if ($args{parent_id}) {
255 # parent_id is always exact. default to positive match
256 if ($args{parent_id} =~ /^NOT:/) {
257 $args{parent_id} =~ s/^NOT://;
258 push @matchtypes, '<>';
259 } else {
260 push @matchtypes, '=';
261 }
262 push @fields, 's.parent_id';
263 push @vals, $args{parent_id};
264 }
265
[909]266 # Filter on "available", because we can.
[946]267 if (defined($args{available})) {
[928]268 # Flex input; accept 1 or 0 as y/n respectively.
269 $args{available} =~ tr/10/yn/;
270 if ($args{available} =~ /^[yn]$/) {
271 push @fields, "s.available";
272 push @matchtypes, '=';
273 push @vals, $args{available};
274 }
[909]275 }
276
[927]277 my $cols = "s.cidr, s.custid, s.type, s.city, s.description, s.id, s.parent_id, s.available, a.vrf, at.dispname";
278
279 # Validation and SQL field name mapping all in one!
280 my %validcols = (cidr => 's.cidr', custid => 's.custid', oldcustid => 's.oldcustid', type => 's.type', city => 's.city',
281 description => 's.description', notes => 's.notes', circuitid => 's.circuitid', vrf => 'a.vrf', vlan => 's.vlan',
282 id => 's.id', parent_id => 's.parent_id', master_id => 's.master_id', available => 's.available');
283 my @usercols;
284
285 if ($args{retfields}) {
286 # caller wants a custom set of returned fields
287 if (ref($args{retfields}) eq ref([])) {
288 # field list passed as list/array
289 foreach (@{$args{retfields}}) {
290 push @usercols, $validcols{$_} if $validcols{$_};
291 }
292 } elsif (not ref $args{retfields}) {
293 # field list passed as simple string
294 foreach (split /\s+/, $args{retfields}) {
295 push @usercols, $validcols{$_} if $validcols{$_};
296 }
297 } else {
298 # nonfatal fail. only accepts array or string. fall back to default list
299 }
300 }
301
302 # only replace the default set if a custom set was passed in
303 $cols = join ', ', @usercols if @usercols;
304
305 my $sql = qq(SELECT $cols FROM searchme s JOIN alloctypes at ON s.type = at.type JOIN allocations a ON s.master_id=a.id);
[903]306 my @sqlcriteria;
307 for (my $i = 0; $i <= $#fields; $i++) {
308 push @sqlcriteria, "$fields[$i] $matchtypes[$i] ?";
309 }
310 $sql .= " WHERE ".join(' AND ', @sqlcriteria) if @sqlcriteria;
311
[908]312 # multifield sorting!
313 if ($args{order}) {
314 my @ordfields = split /,/, $args{order};
315 # there are probably better ways to do this
316 my %omap = (cidr => 's.cidr', net => 's.cidr', network => 's.cidr', ip => 's.cidr',
317 custid => 's.custid', type => 's.type', city => 's.city',
318 desc => 's.description', description => 's.description');
319 my @ordlist;
320 # only pass sort field values from the list of acceptable field names or aliases as per %omap
321 foreach my $ord (@ordfields) {
322 push @ordlist, $omap{$ord}
323 if grep /^$ord$/, (keys %omap);
324 }
325 if (@ordlist) {
326 $sql .= " ORDER BY ". join(',', @ordlist);
327 }
328 }
329
[903]330 my $result = $ip_dbh->selectall_arrayref($sql, {Slice=>{}}, @vals);
331 die $ip_dbh->errstr if !$result;
332
333 return $result;
334} # rpc_search()
[910]335
336
337__END__
338
339=pod
340
341=head1 IPDB XMLRPC Search
342
343This is a general-purpose search API for IPDB. It is currently being extended based on requirements from other tools needing to
344search for data in IPDB.
345
346It supports one XMLRPC sub, "search".
347
348The calling URL for this API should end with "/search-rpc.cgi". If you are doing many requests, you should use the FastCGI variant
349with .fcgi instead of .cgi.
350
351=head2 Calling conventions
352
353IPDB RPC services use "XMLRPC", http://xmlrpc.com, for data exchange.
354
355Arguments are passed in as a key-value list, and data is returned as an array of hashes in some form.
356
357=over 4
358
359=item Perl
360
361 use Frontier::Client;
362 my $server = Frontier::Client->new(
363 url => "http://server/path/search-rpc.cgi",
364 );
365 my %args = (
366 rpcsystem => 'somesystem',
367 rpcuser => 'someuser',
368 arg1 => 'val1',
369 arg2 => 'val2',
370 );
371 my $result = $server->call('ipdb.search', %args);
372
373=item Python 2
374
375 import xmlrpclib
376 server = xmlrpclib.Server("http://server/path/search-rpc.cgi")
377 result = server.ipdb.search(
378 'rpcsystem', 'comesystems',
379 'rpcuser', 'someuser',
380 'arg1', 'val1',
381 'arg2', 'val2',
382 )
383
384=item Python 3
385
386 import xmlrpc.client
387 server = xmlrpc.client.ServerProxy("http://server/path/search-rpc.cgi")
388 result = server.ipdb.search(
389 'rpcsystem', 'somesystem',
390 'rpcuser', 'someuser',
391 'arg1', 'val1',
392 'arg2', 'val2',
393 )
394
395=back
396
397=head3 Standard arguments
398
399The C<rpcsystem> argument is required, and C<rpcuser> is strongly recommended as it may be used for access control in some future
400updates.
401
402C<rpcsystem> must match a configuration entry in the IPDB configuration, and a given string may only be used from an IP listed under
403that configuration entry.
404
405=head2 Search fields and metaoperators
406
407Not all fields are exposed for search. For most purposes these should be sufficient.
408
[925]409Most fields support EXACT: or NOT: prefixes on the search term to restrict the matches.
410
[910]411=over 4
412
413=item cidr
414
415A full or partial CIDR network or IP address. Valid formats include:
416
417=over 4
418
419=item Complete CIDR network, eg 192.168.2.0/24
420
421Returns an exact match for the passed CIDR network.
422
423If prefixed with "CONTAINS:", the containing netblocks up to the master block
424will also be returned.
425
426If prefixed with "WITHIN:", any suballocations in that IP range will be returned.
427
428=item Partial/short CIDR specification with mask length, eg 192.168.3/27
429
430Returns all /27 assignments within 192.168.3.0/24.
431
432=item Partial/short CIDR specification, eg 192.168.4
433
434Returns all assignments matching that leading partial string. Note that 192.168.4 will also return 192.168.40.0/24 through
435192.168.49.0/24 as well as the obvious 192.168.4.0/24.
436
437=item Bare IP address with no mask, eg 192.168.5.42
438
439Returns all assignments containing that IP.
440
441=back
442
443=item custid
444
445Match on a customer ID. Defaults to a partial match.
446
447=item type
448
449Match the two-character internal allocation type identifier.
450
451Defaults to an exact match. Replace the first character with a dot or underscore, or leave it off, to match all subtypes of a
452class; eg .i will return all types of static IP assignments.
453
454A full list of current allocation types is available from the main RPC API's getTypeList sub.
455
456=item city
457
458Matches in the city string.
459
460=item description
461
462Matches in the description string.
463
464=item notes
465
466Matches in the notes field.
467
468=item available
469
470Only useful for static IPs. For historic and architectural reasons, unallocated static IPs are included in general search results.
471Specify 'y' or 'n' to return only unallocated or allocated static IPs respectively.
472
473To search for a free block, use the main RPC API's listFree or findAllocateFrom subs.
474
[925]475=item parent_id
476
477Restrict to allocations in the given parent.
478
[910]479=item order
480
481Sort order specification. Send a string of comma-separated field names for subsorting. Valid sort fields are cidr, custid, type,
482city, and description.
483
[927]484=item fields
485
486Specify the fields to return from the search. By default, these are returned:
487
488=over 4
489
490cidr
491custid
492type
493city
494description
495id
496parent_id
497available
498vrf
499dispname (the "display name" for the type)
500
[910]501=back
502
[927]503The following are available from this interface:
504
505=over 4
506
507cidr
508custid
509oldcustid
510type
511city
512description
513notes
514circuitid
515vrf
516vlan
517id
518parent_id
519master_id
520available
521
522=back
523
524The list may be sent as a space-separated string or as an array. Unknown field names will be ignored.
525
526=back
Note: See TracBrowser for help on using the repository browser.