Coverage for jutil/request.py: 52%

88 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-07 16:40 -0500

1import logging 

2from typing import Tuple, Union, Optional 

3from django.conf import settings 

4import requests 

5import socket 

6from django.http.request import HttpRequest 

7from ipware import get_client_ip # type: ignore 

8 

9try: 

10 from rest_framework.request import Request # type: ignore 

11except Exception as err: 

12 raise Exception("Using jutil.request requires djangorestframework installed") from err 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17class GeoIP: 

18 """ 

19 Basic geolocation information of IP-address. 

20 """ 

21 

22 ip: str 

23 country_name: str 

24 country_code: str 

25 time_zone: str 

26 city: str 

27 zip_code: str 

28 latitude: float 

29 longitude: float 

30 

31 def __init__( # pylint: disable=too-many-arguments 

32 self, 

33 ip: str, 

34 country_name: str, 

35 country_code: str, 

36 time_zone: str, 

37 city: str, 

38 zip_code: str, 

39 latitude: float, 

40 longitude: float, 

41 ): 

42 self.ip = ip 

43 self.country_name = country_name 

44 self.country_code = country_code 

45 self.time_zone = time_zone 

46 self.city = city 

47 self.zip_code = zip_code 

48 self.latitude = latitude 

49 self.longitude = longitude 

50 

51 

52def get_ip(request: Union[HttpRequest, Request]) -> str: 

53 """ 

54 Returns best-guess IP for given request. 

55 Uses ipware library get_client_ip. 

56 If you need to know is IP routable or not, use ipware get_client_ip directly. 

57 See ipware documentation for more info. 

58 

59 Note: Why such a simple function wrapper? I'm generally against wrappers like this, 

60 but in this case made an exceptions: I used to use ipware get_real_ip() everywhere before 

61 it was deprecated and had quite big update process to change all code to use ipware get_client_ip. 

62 I want to avoid such process again so added this wrapper. 

63 

64 :param request: Django's HttpRequest or DRF Request 

65 :return: IP-address or None 

66 """ 

67 return get_client_ip(request)[0] 

68 

69 

70def get_geo_ip_from_ipgeolocation(ip: str, timeout: int = 10, verbose: bool = False) -> GeoIP: 

71 """ 

72 Returns geo IP info or empty dict if geoip query fails. 

73 Uses ipgeolocation.io API and requires settings.IPGEOLOCATION_API_KEY set. 

74 

75 :param ip: str 

76 :param timeout: timeout in seconds 

77 :return: IPGeoInfo 

78 """ 

79 if not hasattr(settings, "IPGEOLOCATION_API_KEY") or not settings.IPGEOLOCATION_API_KEY: 79 ↛ 80line 79 didn't jump to line 80, because the condition on line 79 was never true

80 raise Exception("get_geo_ip_ipstack() requires IPGEOLOCATION_API_KEY defined in Django settings") 

81 url = f"https://api.ipgeolocation.io/ipgeo?apiKey={settings.IPGEOLOCATION_API_KEY}&ip={ip}" 

82 res = requests.get(url, timeout=timeout) 

83 if verbose: 83 ↛ 84line 83 didn't jump to line 84, because the condition on line 83 was never true

84 logger.info("GET %s HTTP %s: %s", url, res.status_code, res.text) 

85 if res.status_code != 200: 85 ↛ 86line 85 didn't jump to line 86, because the condition on line 85 was never true

86 logger.error("get_geo_ip_from_ipgeolocation(%s) failed: %s", ip, res.text) 

87 raise Exception("api.ipgeolocation.io HTTP {}".format(res.status_code)) 

88 data = res.json() 

89 return GeoIP( 

90 ip, 

91 country_name=data["country_name"], 

92 country_code=data["country_code2"], 

93 time_zone=(data.get("time_zone") or {}).get("name") or "", 

94 city=data.get("city") or "", 

95 zip_code=data.get("zipcode") or "", 

96 latitude=float(data["latitude"]), 

97 longitude=float(data["longitude"]), 

98 ) 

99 

100 

101def get_geo_ip_from_ipstack(ip: str, timeout: int = 10, verbose: bool = False) -> GeoIP: 

102 """ 

103 Returns geo IP info or empty dict if geoip query fails. 

104 Uses ipstack.com API and requires settings.IPSTACK_TOKEN set. 

105 

106 :param ip: str 

107 :param timeout: timeout in seconds 

108 :return: IPGeoInfo 

109 """ 

110 if not hasattr(settings, "IPSTACK_TOKEN") or not settings.IPSTACK_TOKEN: 

111 raise Exception("get_geo_ip_from_ipstack() requires IPSTACK_TOKEN defined in Django settings") 

112 url = f"http://api.ipstack.com/{ip}?access_key={settings.IPSTACK_TOKEN}&format=1" 

113 res = requests.get(url, timeout=timeout) 

114 if verbose: 

115 logger.info("GET %s HTTP %s: %s", url, res.status_code, res.text) 

116 if res.status_code != 200: 

117 logger.error("get_geo_ip_from_ipstack(%s) failed: %s", ip, res.text) 

118 raise Exception("api.ipstack.com HTTP {}".format(res.status_code)) 

119 data = res.json() 

120 res_success = data.get("success", True) 

121 if not res_success: 

122 res_info = data.get("info") or "" 

123 logger.error("get_geo_ip_from_ipstack(%s) failed: %s", ip, res_info) 

124 raise Exception(res_info) 

125 return GeoIP( 

126 ip, 

127 country_name=data["country_name"], 

128 country_code=data["country_code"], 

129 time_zone=data.get("time_zone") or "", 

130 city=data.get("city") or "", 

131 zip_code=data.get("zip") or "", 

132 latitude=float(data["latitude"]), 

133 longitude=float(data["longitude"]), 

134 ) 

135 

136 

137def get_geo_ip(ip: str, timeout: int = 10, verbose: bool = False) -> GeoIP: 

138 """ 

139 Returns geo IP info. Raises Exception if query fails. 

140 Uses either ipgeolocation.io (if IPGEOLOCATION_API_KEY set) or ipstack.com (if IPSTACK_TOKEN set) 

141 

142 Example response (GeoIP.__dict__): 

143 

144 { 

145 "ip": "194.100.27.41", 

146 "country_name": "Finland", 

147 "country_code": "FI", 

148 "time_zone": "Europe/Helsinki", 

149 "city": "Helsinki", 

150 "zip_code": "00100", 

151 "latitude": "60.17116", 

152 "longitude": "24.93265" 

153 } 

154 

155 :param ip: str 

156 :param timeout: timeout in seconds 

157 :return: IPGeoInfo 

158 """ 

159 if hasattr(settings, "IPGEOLOCATION_API_KEY") and settings.IPGEOLOCATION_API_KEY: 159 ↛ 161line 159 didn't jump to line 161, because the condition on line 159 was never false

160 return get_geo_ip_from_ipgeolocation(ip, timeout, verbose=verbose) 

161 if hasattr(settings, "IPSTACK_TOKEN") and settings.IPSTACK_TOKEN: 

162 return get_geo_ip_from_ipstack(ip, timeout, verbose=verbose) 

163 raise Exception("get_geo_ip() requires either IPGEOLOCATION_TOKEN or IPSTACK_TOKEN defined in Django settings") 

164 

165 

166def get_geo_ip_or_none(ip: str, timeout: int = 10) -> Optional[GeoIP]: 

167 """ 

168 Returns geo IP info or None if geoip query fails. 

169 Uses either ipgeolocation.io (if IPGEOLOCATION_API_KEY set) or ipstack.com (if IPSTACK_TOKEN set) 

170 

171 :param ip: str 

172 :param timeout: timeout in seconds 

173 :return: Optional[IPGeoInfo] 

174 """ 

175 try: 

176 return get_geo_ip(ip, timeout) 

177 except Exception as err: 

178 logger.error("get_geo_ip_or_none(%s) failed: %s", ip, err) 

179 return None 

180 

181 

182def get_ip_info(ip: str, exceptions: bool = False, timeout: int = 10) -> Tuple[str, str, str]: 

183 """ 

184 Returns (ip, country_code, host) tuple of the IP address. 

185 Uses either ipgeolocation.io (if IPGEOLOCATION_API_KEY set) or ipstack.com (if IPSTACK_TOKEN set) 

186 

187 :param ip: IP address 

188 :param exceptions: Raise Exception or not 

189 :param timeout: Timeout in seconds. Note that timeout only affects geo IP part, not getting host name. 

190 :return: (ip, country_code, host) 

191 """ 

192 if not ip: # localhost 192 ↛ 193line 192 didn't jump to line 193, because the condition on line 192 was never true

193 return "", "", "" 

194 host, country_code = "", "" 

195 try: 

196 geo = get_geo_ip(ip, timeout=timeout) 

197 country_code = geo.country_code 

198 if ip: 198 ↛ 205line 198 didn't jump to line 205, because the condition on line 198 was never false

199 host_info = socket.gethostbyaddr(ip) 

200 host = host_info[0][:255] 

201 except Exception as e: 

202 logger.error("get_ip_info(%s) failed: %s", ip, e) 

203 if exceptions: 

204 raise e 

205 return ip, country_code, host