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

Last change on this file since 927 was 927, checked in by Kris Deugau, 5 years ago

/trunk

Fix mismatch between the web UI search and the RPC search; the RPC search

incorrectly used the leaf allocation entry, not the master VRF entry.

Extend the RPC search to allow specifying the fields to return

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