Skip to content

Commit 9117272

Browse files
committed
Use ua-parser instead of user-agents
1 parent 41b3a89 commit 9117272

File tree

5 files changed

+69
-25
lines changed

5 files changed

+69
-25
lines changed

CHANGELOG.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
# Unreleased
22

33
- Add support for Python 3.13.
4-
- Remove **django-ipware** dependency to reduce the risk of IP spoofing.
5-
(for the same reason Django
4+
5+
Backward-incompatible changes:
6+
7+
- The IP is now read directly from the value of `REMOTE_ADDR`
8+
(instead of relying on **django-ipware**)
9+
for the same reason Django
610
[removed](https://docs.djangoproject.com/en/5.2/releases/1.1/#removed-setremoteaddrfromforwardedfor-middleware)
7-
`SetRemoteAddrFromForwardedFor` in 1.1).
8-
The IP is now extracted directly from the `REMOTE_ADDR` header.
9-
Instead of relying on **django-ipware**
10-
to extract the IP address from the request,
11-
you should configure your web server
12-
to pass the real IP address in the `REMOTE_ADDR` header.
11+
`SetRemoteAddrFromForwardedFor` middleware in 1.1.
12+
If you are using a reverse proxy,
13+
you should configure it
14+
to pass the real IP address in the `REMOTE_ADDR` header,
15+
or you can write a custom version of
16+
[`SetRemoteAddrFromForwardedFor` middleware](https://github.com/django/django/blob/91f18400cc0fb37659e2dbaab5484ff2081f1f30/django/middleware/http.py#L33)
17+
which suits your environment.
18+
- `session.device` is now a property instead of a method and returns string.
19+
- The device object can be accessed using the new property `session.device_info`.
20+
- User agent parsing is now done using `ua-parser` instead of `user-agents`.
21+
- The device object is now an instance of `ua_parser.core.Result`
22+
instead of `user_agents.parsers.UserAgent`.
23+
1324

1425
# 1.1.5 (Jun 22, 2024)
1526

README.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,14 @@ Get IP and user agent:
180180
Get user device (parsed user-agent string):
181181
182182
```python
183-
>>> str(session.device())
184-
'K / Android 10 / Chrome Mobile 118.0.0'
185-
>>> session.device().device
183+
>>> session.device
184+
'K / Android 10 / Chrome Mobile 118.0.0.0'
185+
>>> session.device_info.device
186186
Device(family='K', brand='Generic_Android', model='K')
187-
>>> session.device().os
188-
OperatingSystem(family='Android', version=(10,), version_string='10')
189-
>>> session.device().browser
190-
Browser(family='Chrome Mobile', version=(118, 0, 0), version_string='118.0.0')
187+
>>> session.device_info.os
188+
OS(family='Android', major='10', minor=None, patch=None, patch_minor=None)
189+
>>> session.device_info.user_agent
190+
UserAgent(family='Chrome Mobile', major='118', minor='0', patch='0', patch_minor='0')
191191
```
192192
193193
@@ -211,9 +211,15 @@ Admin page:
211211
- `session.updated_at` is not the session's exact last activity. It's
212212
updated each time the session object is saved in DB. (e.g. when user
213213
logs in, or when ip, user agent, or session data changes)
214-
- **django-qsessions** extracts IP directly from the `REMOTE_ADDR` header.
215-
If you are using a reverse proxy, you need to configure it to pass the
216-
real IP address in the `REMOTE_ADDR` header.
214+
- The IP address is directly read from `request.META["REMOTE_ADDR"]`.
215+
If you are using a reverse proxy,
216+
you should configure it
217+
to pass the real IP address in the `REMOTE_ADDR` header.
218+
You can also write a custom middleware
219+
to set `REMOTE_ADDR` from the value of other headers
220+
(`X-Forwarded-For`, `X-Real-IP`, ...)
221+
in a safe way suitable for your environment.
222+
More info: [Why Django removed SetRemoteAddrFromForwardedFor](https://docs.djangoproject.com/en/5.2/releases/1.1/#removed-setremoteaddrfromforwardedfor-middleware).
217223
218224
## Development
219225

qsessions/models.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import cached_property
12
from importlib import import_module
23

34
from django.conf import settings
@@ -70,13 +71,39 @@ def location(self):
7071
def location_info(self):
7172
return geoip.ip_to_location_info(self.ip)
7273

73-
def device(self):
74+
@cached_property
75+
def device_info(self):
7476
"""
7577
Describe the user agent of this session, if any
76-
:rtype: user_agents.parsers.UserAgent | None
78+
:rtype: ua_parser.core.Result | None
7779
"""
7880
if self.user_agent:
79-
import user_agents # late import to avoid import cost
81+
from ua_parser import parse # late import to avoid import cost
8082

81-
return user_agents.parse(self.user_agent)
83+
return parse(self.user_agent)
8284
return None
85+
86+
@cached_property
87+
def device(self) -> str:
88+
if device := self.device_info:
89+
90+
def get_version_string(version_info):
91+
try:
92+
return ".".join(version_info[: version_info.index(None)])
93+
except ValueError:
94+
return ".".join(version_info)
95+
96+
return "{device} / {os} / {browser}".format(
97+
device=device.device.family if device.device else "Other",
98+
os=(
99+
f"{device.os.family} {get_version_string([device.os.major, device.os.minor, device.os.patch, device.os.patch_minor])}"
100+
if device.os
101+
else "Other"
102+
),
103+
browser=(
104+
f"{device.user_agent.family} {get_version_string([device.user_agent.major, device.user_agent.minor, device.user_agent.patch, device.user_agent.patch_minor])}"
105+
if device.user_agent
106+
else "Other"
107+
),
108+
)
109+
return ""

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
license="MIT",
2929
packages=find_packages(".", include=("qsessions", "qsessions.*")),
3030
include_package_data=True,
31-
install_requires=["Django >= 4.2", "user-agents>=1.1.0"],
31+
install_requires=["Django >= 4.2", "ua-parser[regex] >= 1.0.1"],
3232
extras_require={"dev": dev_requirements},
3333
tests_require=dev_requirements,
3434
classifiers=[

tests/test_model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ def test_device():
5959
"Chrome/70.0.3538.102 Safari/537.36"
6060
)
6161
)
62-
device = session.device()
62+
device = session.device_info
6363
assert device.os.family == "Mac OS X"
64-
assert device.browser.family == "Chrome"
64+
assert device.user_agent.family == "Chrome"
6565

6666

6767
@pytest.mark.django_db

0 commit comments

Comments
 (0)