Merge pull request #123 from DaftFuzz/feature/geolocate-ip

Feature/geolocate ip
This commit is contained in:
Thegan Govender
2023-11-04 23:35:24 +02:00
committed by GitHub
5 changed files with 193 additions and 4 deletions

50
backend/routes/utils.js Normal file
View File

@@ -0,0 +1,50 @@
// api.js
const https = require("https");
const axios = require("axios");
const express = require("express");
const router = express.Router();
const geoliteUrlBase = 'https://geolite.info/geoip/v2.1/city';
const geoliteAccountId = process.env.GEOLITE_ACCOUNT_ID;
const geoliteLicenseKey = process.env.GEOLITE_LICENSE_KEY;
//https://stackoverflow.com/a/29268025
const ipRegex = new RegExp(/\b(?!(10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168))(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/);
const agent = new https.Agent({
rejectUnauthorized: (process.env.REJECT_SELF_SIGNED_CERTIFICATES || 'true').toLowerCase() ==='true'
});
const axios_instance = axios.create({
httpsAgent: agent
});
router.post("/geolocateIp", async (req, res) => {
try {
if(!(geoliteAccountId && geoliteLicenseKey)) {
return res.status(501).send('GeoLite information missing!');
}
const { ipAddress } = req.body;
ipRegex.lastIndex = 0;
if(!ipAddress || !ipRegex.test(ipAddress)) {
return res.status(400).send('Invalid IP address sent!');
}
const response = await axios_instance.get(`${geoliteUrlBase}/${ipAddress}`, {
auth: {
username: geoliteAccountId,
password: geoliteLicenseKey
}
});
return res.send(response.data);
} catch (error) {
res.status(503);
res.send(error);
}
});
module.exports = router;

View File

@@ -15,6 +15,7 @@ const ActivityMonitor = require('./tasks/ActivityMonitor');
const SyncTask = require('./tasks/SyncTask');
const BackupTask = require('./tasks/BackupTask');
const logRouter = require('./routes/logging');
const utilsRouter = require('./routes/utils');
const dbInstance = require("./db");
@@ -109,6 +110,7 @@ app.use('/sync', authenticate , syncRouter.router,()=>{/* #swagger.tags = ['Syn
app.use('/stats', authenticate , statsRouter,()=>{/* #swagger.tags = ['Stats']*/}); // mount the API router at /stats, with JWT middleware
app.use('/backup', authenticate , backupRouter.router,()=>{/* #swagger.tags = ['Backup']*/}); // mount the API router at /backup, with JWT middleware
app.use('/logs', authenticate , logRouter.router,()=>{/* #swagger.tags = ['Logs']*/}); // mount the API router at /logs, with JWT middleware
app.use('/utils', authenticate, utilsRouter, ()=>{/* #swagger.tags = ['Utils']*/}); // mount the API router at /utils, with JWT middleware
const swaggerUi = require('swagger-ui-express');
const swaggerDocument = require('./swagger.json');

View File

@@ -0,0 +1,67 @@
import React from "react";
import Loading from "./general/loading";
import { Button, Modal } from "react-bootstrap";
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
export default function IpInfoModal(props) {
let modalBody = <Loading/>;
if(props.geodata) {
modalBody = <Modal.Body>
<div className="StreamInfo">
<TableContainer className="overflow-hidden">
<Table aria-label="collapsible table" >
<TableHead>
<TableRow>
<TableCell/>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell className="py-0 pb-1">City</TableCell>
<TableCell>{props.geodata.city.names['en']}</TableCell>
</TableRow>
<TableRow>
<TableCell className="py-0 pb-1">Country</TableCell>
<TableCell>{props.geodata.country.names['en']}</TableCell>
</TableRow>
<TableRow>
<TableCell className="py-0 pb-1">Postcode</TableCell>
<TableCell>{props.geodata.postal.code}</TableCell>
</TableRow>
<TableRow>
<TableCell className="py-0 pb-1">ISP</TableCell>
<TableCell>{props.geodata.traits.autonomous_system_organization}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</div>
</Modal.Body>
}
return (
<div>
<Modal show={props.show} onHide={() => props.onHide()}>
<Modal.Header closeButton>
<Modal.Title>
Geolocation info for {props.ipAddress}
</Modal.Title>
</Modal.Header>
{modalBody}
<Modal.Footer>
<Button variant="outline-primary" onClick={()=>props.onHide()}>
Close
</Button>
</Modal.Footer>
</Modal>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, {useState,useEffect} from "react";
import { Link } from 'react-router-dom';
import Card from 'react-bootstrap/Card';
import Row from 'react-bootstrap/Row';
@@ -11,7 +11,9 @@ import PauseFillIcon from "remixicon-react/PauseFillIcon";
import { clientData } from "../../../lib/devices";
import Tooltip from "@mui/material/Tooltip";
import IpInfoModal from '../ip-info';
import axios from 'axios';
function ticksToTimeString(ticks) {
// Convert ticks to seconds
@@ -56,7 +58,7 @@ function convertBitrate(bitrate) {
}
}
function sessionCard(props) {
function SessionCard(props) {
const cardStyle = {
backgroundImage: `url(Proxy/Items/Images/Backdrop?id=${(props.data.session.NowPlayingItem.SeriesId ? props.data.session.NowPlayingItem.SeriesId : props.data.session.NowPlayingItem.Id)}&fillHeight=320&fillWidth=213&quality=80), linear-gradient(to right, #00A4DC, #AA5CC3)`,
height:'100%',
@@ -69,9 +71,56 @@ function sessionCard(props) {
height:'100%',
};
const token = localStorage.getItem('token');
const ipv4Regex = new RegExp(/\b(?!(10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168))(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/);
const [modalVisible, setModalVisible] = useState(false);
const [modalData, setModalData] = useState();
const [isRemoteSession, setIsRemoteSession] = useState();
useEffect(() => {
ipv4Regex.lastIndex = 0;
if(ipv4Regex.test(props.data.session.RemoteEndPoint)) {
setIsRemoteSession(true)
}
}, []);
function showModal() {
if(!isRemoteSession) {
return
}
const fetchData = async () => {
const result = await axios.post(`/utils/geolocateIp`, {
ipAddress: props.data.session.RemoteEndPoint
}, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
}
});
setModalData(result.data);
};
if(!modalData) {
fetchData();
}
setModalVisible(true);
}
function hideModal() {
setModalVisible(false);
}
return (
<Card className="stat-card" style={cardStyle}>
<IpInfoModal
show={modalVisible}
onHide={hideModal}
ipAddress={props.data.session.RemoteEndPoint}
geodata={modalData}/>
<div style={cardBgStyle} className="rounded-top">
<Row className="h-100">
<Col className="d-none d-lg-block stat-card-banner">
@@ -94,13 +143,27 @@ function sessionCard(props) {
<Row className="ellipse card-client-version"> {props.data.session.Client + " " + props.data.session.ApplicationVersion}</Row>
<Row className="d-flex flex-column flex-md-row">
<Col className="px-0 col-auto ellipse">{props.data.session.PlayState.PlayMethod}</Col>
<Col className="px-0 px-md-2 col-auto ellipse">{(props.data.session.NowPlayingItem.MediaStreams ? '( '+props.data.session.NowPlayingItem.MediaStreams.find(stream => stream.Type==='Video')?.Codec.toUpperCase()+(props.data.session.TranscodingInfo? ' - '+props.data.session.TranscodingInfo.VideoCodec.toUpperCase() : '')+' - '+convertBitrate(props.data.session.TranscodingInfo ? props.data.session.TranscodingInfo.Bitrate :props.data.session.NowPlayingItem.MediaStreams.find(stream => stream.Type==='Video')?.BitRate)+' )':'')}</Col>
<Col className="px-0 px-md-2 col-auto ellipse">{(props.data.session.NowPlayingItem.MediaStreams ? '( '+props.data.session.NowPlayingItem.MediaStreams.find(stream => stream.Type==='Video')?.Codec.toUpperCase()+(props.data.session.TranscodingInfo? ' - '+props.data.session.TranscodingInfo.VideoCodec.toUpperCase() : '')+' - '+convertBitrate(props.data.session.TranscodingInfo ? props.data.session.TranscodingInfo.Bitrate :props.data.session.NowPlayingItem.MediaStreams.find(stream => stream.Type==='Video')?.BitRate)+' )':'')}</Col>
<Col className="px-0 col-auto ellipse">
<Tooltip title={props.data.session.NowPlayingItem.SubtitleStream}>
<span>
{props.data.session.NowPlayingItem.SubtitleStream}
</span>
</Tooltip>
</Col>
</Row>
<Row>
<Col className="px-0 col-auto ellipse">
{isRemoteSession && (process.env.GEOLITE_ACCOUNT_ID && process.env.GEOLITE_LICENSE_KEY) ?
<Card.Text>
IP Address: <Link onClick={showModal}>{props.data.session.RemoteEndPoint}</Link>
</Card.Text>
:
<span>
IP Address: {props.data.session.RemoteEndPoint}
</span>
}
</Col>
</Row>
@@ -232,4 +295,4 @@ function sessionCard(props) {
);
}
export default sessionCard;
export default SessionCard;

View File

@@ -64,6 +64,13 @@ module.exports = function(app) {
changeOrigin: true,
})
);
app.use(
'/utils',
createProxyMiddleware({
target: `http://127.0.0.1:${process.env.PORT || 3003}`,
changeOrigin: true
})
);
console.log(`Proxy middleware applied`);