问题
In my project, I have a function in postgres (plpgsql) that determines country from a given ip address:
CREATE OR REPLACE FUNCTION get_country_for_ip(character varying)
RETURNS character varying AS
$BODY$
declare
ip ALIAS for $1;
ccode varchar;
cparts varchar[];
nparts bigint[];
addr bigint;
begin
cparts := string_to_array(ip, '.');
if array_upper(cparts, 1) <> 4 then
raise exception 'gcfi01: Invalid IP address: %', ip;
end if;
nparts := array[a2i(cparts[1])::bigint, a2i(cparts[2])::bigint, a2i(cparts[3])::bigint, a2i(cparts[4])::bigint];
if(nparts[1] is null or nparts[1] < 0 or nparts[1] > 255 or
nparts[2] is null or nparts[2] < 0 or nparts[2] > 255 or
nparts[3] is null or nparts[3] < 0 or nparts[3] > 255 or
nparts[4] is null or nparts[4] < 0 or nparts[4] > 255) then
raise exception 'gcfi02: Invalid IP address: %', ip;
end if;
addr := (nparts[1] << 24) | (nparts[2] << 16) | (nparts[3] << 8) | nparts[4];
addr := nparts[1] * 256 * 65536 + nparts[2] * 65536 + nparts[3] * 256 + nparts[4];
select into ccode t_country_code from ip_to_country where addr between n_from and n_to limit 1;
if ccode is null then
ccode := '';
end if;
return ccode;
end;$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
This may not be the most efficient, but it does the job. Note that it uses an internal table (ip_to_country
), which contains data as below (the numbers n_from and n_to are the long
values of the start and end of address ranges:
n_from | n_to | t_country_code
----------+----------+----------------
0 | 16777215 | ZZ
16777216 | 16777471 | AU
...
Now we are starting to look at the IPv6 addressing as well - and I need to add similar functionality for IPv6 addresses. I have a similar set of data for IPv6, which looks like this:
t_start | t_end | t_country_code
-------------+-----------------------------------------+----------------
:: | ff:ffff:ffff:ffff:ffff:ffff:ffff:ffff | ZZ
100:: | 1ff:ffff:ffff:ffff:ffff:ffff:ffff:ffff | ZZ
...
2000:: | 2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff | ZZ
...
2001:1200:: | 2001:1200:ffff:ffff:ffff:ffff:ffff:ffff | MX
...
Now, given an IP address ::1
, how do I (1) check that it's a valid IPv6 address and (2) get the corresponding country mapping?
回答1:
I believe I found the solution. It involves modifying the data first and then some massaging of the input. Here's what worked.
First, the data needs to be converted so that all addresses are full, without shortening, with semicolon separators removed. The sample data shown in my question is converted to:
t_start | t_end | t_country_code
----------------------------------+----------------------------------+----------------
00000000000000000000000000000000 | 00ffffffffffffffffffffffffffffff | ZZ
01000000000000000000000000000000 | 01ffffffffffffffffffffffffffffff | ZZ
...
20000000000000000000000000000000 | 2000ffffffffffffffffffffffffffff | ZZ
...
20011200000000000000000000000000 | 20011200ffffffffffffffffffffffff | MX
...
This is what is stored in the database.
The next step was to convert the IP address received in the code to be in the same format. This is done in PHP with the following code (assume that $ip_address
is the incoming IPv6 address):
$addr_bin = inet_pton($ip_address);
$bytes = unpack('n*', $addr_bin);
$ip_address = implode('', array_map(function ($b) {return sprintf("%04x", $b); }, $bytes));
Now variable $ip_adress
wil contain the full IPv6 address, for example
:: => 00000000000000000000000000000000
2001:1200::ab => 200112000000000000000000000000ab
and so on.
Now you can simply compare this full address with the ranges in the database. I added a second function to the database to deal with IPv6 addresses, which looks like this:
CREATE OR REPLACE FUNCTION get_country_for_ipv6(character varying)
RETURNS character varying AS
$BODY$
declare
ip ALIAS for $1;
ccode varchar;
begin
select into ccode t_country_code from ipv6_to_country where addr between n_from and n_to limit 1;
if ccode is null then
ccode := '';
end if;
return ccode;
end;$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Finally, in my php code I added the code that calls one or the other Postgres function based on the input ip_address.
回答2:
First, I see a couple things you are doing that will pose problems. The first is this use of varchar and long to represent IP addresses when PostgreSQL has perfectly valid INET and CIDR types that will do what you want only better and faster. Note these do not support GIN indexing properly at present so you can't do exclude constraints on them. If you need that, look at the ip4r extension which does support this.
Note as a patch for now you can cast your varchar to inet. Inet also supports both ipv4 and ipv6 addresses as does cidr, and similar types exist on ip4r.
This will solve the ipv6 validation issue for you, and likely cut down on your storage as well as provide better operational checks and better performance.
As for countries, I am also thinking that the mappings may not be so straight-forward.
来源:https://stackoverflow.com/questions/8404357/determine-country-from-ip-ipv6