Skip to content

Commit 17a155b

Browse files
authored
Merge pull request #50 from maxmind/greg/ipaddress-get-support
Add support for ipaddress objects in "get" method
2 parents 6142e41 + a14fed4 commit 17a155b

File tree

6 files changed

+234
-91
lines changed

6 files changed

+234
-91
lines changed

HISTORY.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ History
1010
* The extension source directory was moved to prevent an ``ImportWarning``
1111
when importing the module on Python 2 with ``-Wdefault`` set. Reported by
1212
David Szotten and Craig de Stigter. GitHub #31.
13+
* The ``get`` method now accepts ``ipaddress.IPv4Address`` and
14+
``ipaddress.IPv6Address`` objects in addition to strings. This works with
15+
both the pure Python implementation as well as the extension. Based on a
16+
pull request #48 by Eric Pruitt. GitHub #50.
1317

1418
1.4.1 (2018-06-22)
1519
++++++++++++++++++

extension/maxminddb.c

Lines changed: 135 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#include <Python.h>
22
#include <maxminddb.h>
3+
#include <arpa/inet.h>
4+
#include <netinet/in.h>
5+
#include <sys/socket.h>
36
#include "structmember.h"
47

58
#define __STDC_FORMAT_MACROS
@@ -28,10 +31,12 @@ typedef struct {
2831
PyObject *record_size;
2932
} Metadata_obj;
3033

34+
static bool format_sockaddr(struct sockaddr *addr, char *dst);
3135
static PyObject *from_entry_data_list(MMDB_entry_data_list_s **entry_data_list);
3236
static PyObject *from_map(MMDB_entry_data_list_s **entry_data_list);
3337
static PyObject *from_array(MMDB_entry_data_list_s **entry_data_list);
3438
static PyObject *from_uint128(const MMDB_entry_data_list_s *entry_data_list);
39+
static int ip_converter(PyObject *obj, struct sockaddr_storage *ip_address);
3540

3641
#if PY_MAJOR_VERSION >= 3
3742
#define MOD_INIT(name) PyMODINIT_FUNC PyInit_ ## name(void)
@@ -106,33 +111,29 @@ static int Reader_init(PyObject *self, PyObject *args, PyObject *kwds)
106111

107112
static PyObject *Reader_get(PyObject *self, PyObject *args)
108113
{
109-
char *ip_address = NULL;
114+
MMDB_s *mmdb = ((Reader_obj *)self)->mmdb;
110115

111-
Reader_obj *mmdb_obj = (Reader_obj *)self;
112-
if (!PyArg_ParseTuple(args, "s", &ip_address)) {
116+
if (NULL == mmdb) {
117+
PyErr_SetString(PyExc_ValueError,
118+
"Attempt to read from a closed MaxMind DB.");
113119
return NULL;
114120
}
115121

116-
MMDB_s *mmdb = mmdb_obj->mmdb;
122+
struct sockaddr_storage ip_address_ss = { 0 };
123+
struct sockaddr *ip_address = (struct sockaddr *)&ip_address_ss;
124+
if (!PyArg_ParseTuple(args, "O&", ip_converter, &ip_address_ss)) {
125+
return NULL;
126+
}
117127

118-
if (NULL == mmdb) {
128+
if (!ip_address->sa_family) {
119129
PyErr_SetString(PyExc_ValueError,
120-
"Attempt to read from a closed MaxMind DB.");
130+
"Error parsing argument");
121131
return NULL;
122132
}
123133

124-
int gai_error = 0;
125134
int mmdb_error = MMDB_SUCCESS;
126135
MMDB_lookup_result_s result =
127-
MMDB_lookup_string(mmdb, ip_address, &gai_error,
128-
&mmdb_error);
129-
130-
if (0 != gai_error) {
131-
PyErr_Format(PyExc_ValueError,
132-
"'%s' does not appear to be an IPv4 or IPv6 address.",
133-
ip_address);
134-
return NULL;
135-
}
136+
MMDB_lookup_sockaddr(mmdb, ip_address, &mmdb_error);
136137

137138
if (MMDB_SUCCESS != mmdb_error) {
138139
PyObject *exception;
@@ -141,8 +142,11 @@ static PyObject *Reader_get(PyObject *self, PyObject *args)
141142
} else {
142143
exception = MaxMindDB_error;
143144
}
144-
PyErr_Format(exception, "Error looking up %s. %s",
145-
ip_address, MMDB_strerror(mmdb_error));
145+
char ipstr[INET6_ADDRSTRLEN] = { 0 };
146+
if (format_sockaddr(ip_address, ipstr)) {
147+
PyErr_Format(exception, "Error looking up %s. %s",
148+
ipstr, MMDB_strerror(mmdb_error));
149+
}
146150
return NULL;
147151
}
148152

@@ -153,9 +157,12 @@ static PyObject *Reader_get(PyObject *self, PyObject *args)
153157
MMDB_entry_data_list_s *entry_data_list = NULL;
154158
int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
155159
if (MMDB_SUCCESS != status) {
156-
PyErr_Format(MaxMindDB_error,
157-
"Error while looking up data for %s. %s",
158-
ip_address, MMDB_strerror(status));
160+
char ipstr[INET6_ADDRSTRLEN] = { 0 };
161+
if (format_sockaddr(ip_address, ipstr)) {
162+
PyErr_Format(MaxMindDB_error,
163+
"Error while looking up data for %s. %s",
164+
ipstr, MMDB_strerror(status));
165+
}
159166
MMDB_free_entry_data_list(entry_data_list);
160167
return NULL;
161168
}
@@ -166,6 +173,113 @@ static PyObject *Reader_get(PyObject *self, PyObject *args)
166173
return py_obj;
167174
}
168175

176+
static int ip_converter(PyObject *obj, struct sockaddr_storage *ip_address)
177+
{
178+
#if PY_MAJOR_VERSION >= 3
179+
if (PyUnicode_Check(obj)) {
180+
Py_ssize_t len;
181+
const char *ipstr = PyUnicode_AsUTF8AndSize(obj, &len);
182+
# else
183+
if (PyUnicode_Check(obj) || PyString_Check(obj)) {
184+
// Although this should work on Python 3, we will hopefully delete
185+
// this soon and the Python 3 version is cleaner.
186+
const char *ipstr = PyString_AsString(obj);
187+
Py_ssize_t len = PyString_Size(obj);
188+
#endif
189+
if (!ipstr) {
190+
PyErr_SetString(PyExc_TypeError,
191+
"argument 1 contains an invalid string");
192+
return 0;
193+
}
194+
if (strlen(ipstr) != (size_t)len) {
195+
PyErr_SetString(PyExc_TypeError,
196+
"argument 1 contains an embedded null character");
197+
return 0;
198+
}
199+
200+
struct addrinfo hints = {
201+
.ai_family = AF_UNSPEC,
202+
.ai_flags = AI_NUMERICHOST,
203+
// We set ai_socktype so that we only get one result back
204+
.ai_socktype = SOCK_STREAM
205+
};
206+
207+
struct addrinfo *addresses = NULL;
208+
int gai_status = getaddrinfo(ipstr, NULL, &hints, &addresses);
209+
if (gai_status) {
210+
PyErr_Format(PyExc_ValueError,
211+
"'%s' does not appear to be an IPv4 or IPv6 address.",
212+
ipstr);
213+
return 0;
214+
}
215+
if (!addresses) {
216+
PyErr_SetString(PyExc_RuntimeError,
217+
"getaddrinfo was successful but failed to set the addrinfo");
218+
219+
}
220+
memcpy(ip_address, addresses->ai_addr, addresses->ai_addrlen);
221+
freeaddrinfo(addresses);
222+
return 1;
223+
}
224+
PyObject *packed = PyObject_GetAttrString(obj, "packed");
225+
if (!packed) {
226+
PyErr_SetString(PyExc_TypeError,
227+
"argument 1 must be a string or ipaddress object");
228+
return 0;
229+
}
230+
Py_ssize_t len;
231+
char *bytes;
232+
int status = PyBytes_AsStringAndSize(packed, &bytes, &len);
233+
if (status == -1) {
234+
PyErr_SetString(PyExc_TypeError,
235+
"argument 1 must be a string or ipaddress object");
236+
Py_DECREF(packed);
237+
return 0;
238+
}
239+
240+
switch (len) {
241+
case 16: {
242+
ip_address->ss_family = AF_INET6;
243+
struct sockaddr_in6 *sin = (struct sockaddr_in6 *)ip_address;
244+
memcpy(sin->sin6_addr.s6_addr, bytes, len);
245+
Py_DECREF(packed);
246+
return 1;
247+
}
248+
case 4: {
249+
ip_address->ss_family = AF_INET;
250+
struct sockaddr_in *sin = (struct sockaddr_in *)ip_address;
251+
memcpy(&(sin->sin_addr.s_addr), bytes, len);
252+
Py_DECREF(packed);
253+
return 1;
254+
}
255+
default:
256+
PyErr_SetString(PyExc_ValueError,
257+
"argument 1 returned an unexpected packed length for address");
258+
Py_DECREF(packed);
259+
return 0;
260+
}
261+
}
262+
263+
static bool format_sockaddr(struct sockaddr *sa, char *dst)
264+
{
265+
char *addr;
266+
if (sa->sa_family == AF_INET) {
267+
struct sockaddr_in *sin = (struct sockaddr_in *)sa;
268+
addr = (char *)&sin->sin_addr;
269+
} else {
270+
struct sockaddr_in6 *sin = (struct sockaddr_in6 *)sa;
271+
addr = (char *)&sin->sin6_addr;
272+
}
273+
274+
if (inet_ntop(sa->sa_family, addr, dst, INET6_ADDRSTRLEN)) {
275+
return true;
276+
}
277+
PyErr_SetString(PyExc_RuntimeError,
278+
"unable to format IP address");
279+
return false;
280+
}
281+
282+
169283
static PyObject *Reader_metadata(PyObject *self, PyObject *UNUSED(args))
170284
{
171285
Reader_obj *mmdb_obj = (Reader_obj *)self;

maxminddb/compat.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ def int_from_bytes(b):
2424

2525
string_type = basestring
2626

27-
string_type_name = 'string'
2827
else:
2928

3029
def compat_ip_address(address):
@@ -39,5 +38,3 @@ def compat_ip_address(address):
3938
byte_from_int = lambda x: bytes([x])
4039

4140
string_type = str
42-
43-
string_type_name = string_type.__name__

maxminddb/reader.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515

1616
import struct
1717

18-
from maxminddb.compat import (byte_from_int, compat_ip_address, string_type,
19-
string_type_name)
18+
from maxminddb.compat import byte_from_int, compat_ip_address, string_type
2019
from maxminddb.const import MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY, MODE_FD
2120
from maxminddb.decoder import Decoder
2221
from maxminddb.errors import InvalidDatabaseError
@@ -105,23 +104,25 @@ def get(self, ip_address):
105104
Arguments:
106105
ip_address -- an IP address in the standard string notation
107106
"""
108-
if not isinstance(ip_address, string_type):
109-
raise TypeError('argument 1 must be %s, not %s' %
110-
(string_type_name, type(ip_address).__name__))
107+
if isinstance(ip_address, string_type):
108+
address = compat_ip_address(ip_address)
109+
else:
110+
address = ip_address
111111

112-
address = compat_ip_address(ip_address)
112+
try:
113+
packed_address = bytearray(address.packed)
114+
except AttributeError:
115+
raise TypeError('argument 1 must be a string or ipaddress object')
113116

114117
if address.version == 6 and self._metadata.ip_version == 4:
115118
raise ValueError(
116119
'Error looking up {0}. You attempted to look up '
117120
'an IPv6 address in an IPv4-only database.'.format(ip_address))
118-
pointer = self._find_address_in_tree(address)
121+
pointer = self._find_address_in_tree(packed_address)
119122

120123
return self._resolve_data_pointer(pointer) if pointer else None
121124

122-
def _find_address_in_tree(self, ip_address):
123-
packed = bytearray(ip_address.packed)
124-
125+
def _find_address_in_tree(self, packed):
125126
bit_count = len(packed) * 8
126127
node = self._start_node(bit_count)
127128

tests/data

Submodule data updated 59 files

0 commit comments

Comments
 (0)