diff --git a/docker/docker-compose-dev.yml b/docker/docker-compose-dev.yml index 442680195..e82828fdb 100644 --- a/docker/docker-compose-dev.yml +++ b/docker/docker-compose-dev.yml @@ -21,7 +21,9 @@ services: command: ./gbans serve postgres: - image: postgis/postgis:15-3.3 + build: + context: "." + dockerfile: postgres-ip4r.Dockerfile restart: always shm_size: 1gb ports: diff --git a/docker/postgres-ip4r.Dockerfile b/docker/postgres-ip4r.Dockerfile new file mode 100644 index 000000000..9783b2367 --- /dev/null +++ b/docker/postgres-ip4r.Dockerfile @@ -0,0 +1,7 @@ +FROM postgres:15-bullseye + +RUN apt-get update \ + && apt-cache showpkg postgresql-$PG_MAJOR-ip4r \ + && apt-get install -y --no-install-recommends \ + postgresql-$PG_MAJOR-ip4r \ + && rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index adf9e20f3..e666d10bd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,7 +32,7 @@ "@date-io/date-fns": "^2.17.0", "@ebay/nice-modal-react": "^1.2.13", "@emotion/react": "^11.11.4", - "@emotion/styled": "^11.11.0", + "@emotion/styled": "^11.11.5", "@eslint/js": "^8.57.0", "@fontsource/roboto": "^5.0.12", "@loadable/component": "^5.16.3", @@ -51,8 +51,8 @@ "@types/loadable__component": "^5.13.9", "@types/lodash-es": "^4.17.12", "@types/minimatch": "^5.1.2", - "@types/node": "^20.11.30", - "@types/react": "^18.2.73", + "@types/node": "^20.12.3", + "@types/react": "^18.2.74", "@types/react-dom": "^18.2.23", "@types/steamid": "^2.0.3", "@types/video-react": "^0.15.6", @@ -95,7 +95,7 @@ "string-to-color": "^2.2.2", "typescript": "^5.4.3", "video-react": "^0.16.0", - "vite": "^5.2.6", + "vite": "^5.2.7", "vite-plugin-html": "^3.2.2", "vitest": "^1.4.0", "yup": "^1.4.0" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b7b90b747..5db34bee5 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -16,10 +16,10 @@ devDependencies: version: 1.2.13(react-dom@18.2.0)(react@18.2.0) '@emotion/react': specifier: ^11.11.4 - version: 11.11.4(@types/react@18.2.73)(react@18.2.0) + version: 11.11.4(@types/react@18.2.74)(react@18.2.0) '@emotion/styled': - specifier: ^11.11.0 - version: 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) + specifier: ^11.11.5 + version: 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.74)(react@18.2.0) '@eslint/js': specifier: ^8.57.0 version: 8.57.0 @@ -31,25 +31,25 @@ devDependencies: version: 5.16.3(react@18.2.0) '@mui/icons-material': specifier: ^5.15.14 - version: 5.15.14(@mui/material@5.15.14)(@types/react@18.2.73)(react@18.2.0) + version: 5.15.14(@mui/material@5.15.14)(@types/react@18.2.74)(react@18.2.0) '@mui/lab': specifier: 5.0.0-alpha.165 - version: 5.0.0-alpha.165(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.14)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + version: 5.0.0-alpha.165(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@5.15.14)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) '@mui/material': specifier: ^5.15.14 - version: 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + version: 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) '@mui/system': specifier: ^5.15.14 - version: 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react@18.2.0) + version: 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react@18.2.0) '@mui/utils': specifier: ^5.15.14 - version: 5.15.14(@types/react@18.2.73)(react@18.2.0) + version: 5.15.14(@types/react@18.2.74)(react@18.2.0) '@mui/x-charts': specifier: ^6.19.8 - version: 6.19.8(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.14)(@mui/system@5.15.14)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + version: 6.19.8(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@5.15.14)(@mui/system@5.15.14)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) '@mui/x-date-pickers': specifier: ^6.19.8 - version: 6.19.8(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.14)(@mui/system@5.15.14)(@types/react@18.2.73)(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0) + version: 6.19.8(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@5.15.14)(@mui/system@5.15.14)(@types/react@18.2.74)(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0) '@sentry/react': specifier: ^7.109.0 version: 7.109.0(react@18.2.0) @@ -75,11 +75,11 @@ devDependencies: specifier: ^5.1.2 version: 5.1.2 '@types/node': - specifier: ^20.11.30 - version: 20.11.30 + specifier: ^20.12.3 + version: 20.12.3 '@types/react': - specifier: ^18.2.73 - version: 18.2.73 + specifier: ^18.2.74 + version: 18.2.74 '@types/react-dom': specifier: ^18.2.23 version: 18.2.23 @@ -97,7 +97,7 @@ devDependencies: version: 6.21.0(eslint@8.57.0)(typescript@5.4.3) '@vitejs/plugin-react-swc': specifier: ^3.6.0 - version: 3.6.0(vite@5.2.6) + version: 3.6.0(vite@5.2.7) base64-js: specifier: ^1.5.1 version: 1.5.1 @@ -160,7 +160,7 @@ devDependencies: version: 9.0.4 mui-markdown: specifier: ^1.1.13 - version: 1.1.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.14)(markdown-to-jsx@7.4.5)(react-dom@18.2.0)(react@18.2.0) + version: 1.1.13(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@5.15.14)(markdown-to-jsx@7.4.5)(react-dom@18.2.0)(react@18.2.0) prettier: specifier: ^3.2.5 version: 3.2.5 @@ -207,14 +207,14 @@ devDependencies: specifier: ^0.16.0 version: 0.16.0(react-dom@18.2.0)(react@18.2.0) vite: - specifier: ^5.2.6 - version: 5.2.6(@types/node@20.11.30) + specifier: ^5.2.7 + version: 5.2.7(@types/node@20.12.3) vite-plugin-html: specifier: ^3.2.2 - version: 3.2.2(vite@5.2.6) + version: 3.2.2(vite@5.2.7) vitest: specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.30) + version: 1.4.0(@types/node@20.12.3) yup: specifier: ^1.4.0 version: 1.4.0 @@ -411,7 +411,7 @@ packages: '@babel/runtime': 7.23.9 '@emotion/hash': 0.9.1 '@emotion/memoize': 0.8.1 - '@emotion/serialize': 1.1.3 + '@emotion/serialize': 1.1.4 babel-plugin-macros: 3.1.0 convert-source-map: 1.9.0 escape-string-regexp: 4.0.0 @@ -434,8 +434,8 @@ packages: resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} dev: true - /@emotion/is-prop-valid@1.2.1: - resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==} + /@emotion/is-prop-valid@1.2.2: + resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} dependencies: '@emotion/memoize': 0.8.1 dev: true @@ -444,7 +444,7 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: true - /@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0): + /@emotion/react@11.11.4(@types/react@18.2.74)(react@18.2.0): resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==} peerDependencies: '@types/react': '*' @@ -460,7 +460,7 @@ packages: '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 '@emotion/weak-memoize': 0.3.1 - '@types/react': 18.2.73 + '@types/react': 18.2.74 hoist-non-react-statics: 3.3.2 react: 18.2.0 dev: true @@ -475,12 +475,22 @@ packages: csstype: 3.1.3 dev: true + /@emotion/serialize@1.1.4: + resolution: {integrity: sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==} + dependencies: + '@emotion/hash': 0.9.1 + '@emotion/memoize': 0.8.1 + '@emotion/unitless': 0.8.1 + '@emotion/utils': 1.2.1 + csstype: 3.1.3 + dev: true + /@emotion/sheet@1.2.2: resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} dev: true - /@emotion/styled@11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0): - resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} + /@emotion/styled@11.11.5(@emotion/react@11.11.4)(@types/react@18.2.74)(react@18.2.0): + resolution: {integrity: sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==} peerDependencies: '@emotion/react': ^11.0.0-rc.0 '@types/react': '*' @@ -491,12 +501,12 @@ packages: dependencies: '@babel/runtime': 7.23.9 '@emotion/babel-plugin': 11.11.0 - '@emotion/is-prop-valid': 1.2.1 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/serialize': 1.1.3 + '@emotion/is-prop-valid': 1.2.2 + '@emotion/react': 11.11.4(@types/react@18.2.74)(react@18.2.0) + '@emotion/serialize': 1.1.4 '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 - '@types/react': 18.2.73 + '@types/react': 18.2.74 react: 18.2.0 dev: true @@ -872,7 +882,7 @@ packages: react-is: 16.13.1 dev: true - /@mui/base@5.0.0-beta.36(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + /@mui/base@5.0.0-beta.36(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-6A8fYiXgjqTO6pgj31Hc8wm1M3rFYCxDRh09dBVk0L0W4cb2lnurRJa3cAyic6hHY+we1S58OdGYRbKmOsDpGQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -885,17 +895,17 @@ packages: dependencies: '@babel/runtime': 7.23.9 '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) - '@mui/types': 7.2.13(@types/react@18.2.73) - '@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.2.74) + '@mui/utils': 5.15.14(@types/react@18.2.74)(react@18.2.0) '@popperjs/core': 2.11.8 - '@types/react': 18.2.73 + '@types/react': 18.2.74 clsx: 2.1.0 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@mui/base@5.0.0-beta.40(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + /@mui/base@5.0.0-beta.40(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -908,10 +918,10 @@ packages: dependencies: '@babel/runtime': 7.23.9 '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) - '@mui/types': 7.2.14(@types/react@18.2.73) - '@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.74) + '@mui/utils': 5.15.14(@types/react@18.2.74)(react@18.2.0) '@popperjs/core': 2.11.8 - '@types/react': 18.2.73 + '@types/react': 18.2.74 clsx: 2.1.0 prop-types: 15.8.1 react: 18.2.0 @@ -922,7 +932,7 @@ packages: resolution: {integrity: sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA==} dev: true - /@mui/icons-material@5.15.14(@mui/material@5.15.14)(@types/react@18.2.73)(react@18.2.0): + /@mui/icons-material@5.15.14(@mui/material@5.15.14)(@types/react@18.2.74)(react@18.2.0): resolution: {integrity: sha512-vj/51k7MdFmt+XVw94sl30SCvGx6+wJLsNYjZRgxhS6y3UtnWnypMOsm3Kmg8TN+P0dqwsjy4/fX7B1HufJIhw==} engines: {node: '>=12.0.0'} peerDependencies: @@ -934,12 +944,12 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.73 + '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.74 react: 18.2.0 dev: true - /@mui/lab@5.0.0-alpha.165(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.14)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + /@mui/lab@5.0.0-alpha.165(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@5.15.14)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-8/zJStT10nh9yrAzLOPTICGhpf5YiGp/JpM0bdTP7u5AE+YT+X2u6QwMxuCrVeW8/WVLAPFg0vtzyfgPcN5T7g==} engines: {node: '>=12.0.0'} peerDependencies: @@ -958,21 +968,21 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - '@mui/base': 5.0.0-beta.36(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react@18.2.0) - '@mui/types': 7.2.13(@types/react@18.2.73) - '@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0) - '@types/react': 18.2.73 + '@emotion/react': 11.11.4(@types/react@18.2.74)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.74)(react@18.2.0) + '@mui/base': 5.0.0-beta.36(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react@18.2.0) + '@mui/types': 7.2.13(@types/react@18.2.74) + '@mui/utils': 5.15.14(@types/react@18.2.74)(react@18.2.0) + '@types/react': 18.2.74 clsx: 2.1.0 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@mui/material@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + /@mui/material@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-kEbRw6fASdQ1SQ7LVdWR5OlWV3y7Y54ZxkLzd6LV5tmz+NpO3MJKZXSfgR0LHMP7meKsPiMm4AuzV0pXDpk/BQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -990,14 +1000,14 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - '@mui/base': 5.0.0-beta.40(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.74)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.74)(react@18.2.0) + '@mui/base': 5.0.0-beta.40(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) '@mui/core-downloads-tracker': 5.15.14 - '@mui/system': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react@18.2.0) - '@mui/types': 7.2.14(@types/react@18.2.73) - '@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0) - '@types/react': 18.2.73 + '@mui/system': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.74) + '@mui/utils': 5.15.14(@types/react@18.2.74)(react@18.2.0) + '@types/react': 18.2.74 '@types/react-transition-group': 4.4.10 clsx: 2.1.0 csstype: 3.1.3 @@ -1008,7 +1018,7 @@ packages: react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) dev: true - /@mui/private-theming@5.15.14(@types/react@18.2.73)(react@18.2.0): + /@mui/private-theming@5.15.14(@types/react@18.2.74)(react@18.2.0): resolution: {integrity: sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==} engines: {node: '>=12.0.0'} peerDependencies: @@ -1019,13 +1029,13 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0) - '@types/react': 18.2.73 + '@mui/utils': 5.15.14(@types/react@18.2.74)(react@18.2.0) + '@types/react': 18.2.74 prop-types: 15.8.1 react: 18.2.0 dev: true - /@mui/styled-engine@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0): + /@mui/styled-engine@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0): resolution: {integrity: sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==} engines: {node: '>=12.0.0'} peerDependencies: @@ -1040,14 +1050,14 @@ packages: dependencies: '@babel/runtime': 7.23.9 '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.74)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.74)(react@18.2.0) csstype: 3.1.3 prop-types: 15.8.1 react: 18.2.0 dev: true - /@mui/system@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react@18.2.0): + /@mui/system@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react@18.2.0): resolution: {integrity: sha512-auXLXzUaCSSOLqJXmsAaq7P96VPRXg2Rrz6OHNV7lr+kB8lobUF+/N84Vd9C4G/wvCXYPs5TYuuGBRhcGbiBGg==} engines: {node: '>=12.0.0'} peerDependencies: @@ -1064,20 +1074,20 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - '@mui/private-theming': 5.15.14(@types/react@18.2.73)(react@18.2.0) - '@mui/styled-engine': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@mui/types': 7.2.14(@types/react@18.2.73) - '@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0) - '@types/react': 18.2.73 + '@emotion/react': 11.11.4(@types/react@18.2.74)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.74)(react@18.2.0) + '@mui/private-theming': 5.15.14(@types/react@18.2.74)(react@18.2.0) + '@mui/styled-engine': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.74) + '@mui/utils': 5.15.14(@types/react@18.2.74)(react@18.2.0) + '@types/react': 18.2.74 clsx: 2.1.0 csstype: 3.1.3 prop-types: 15.8.1 react: 18.2.0 dev: true - /@mui/types@7.2.13(@types/react@18.2.73): + /@mui/types@7.2.13(@types/react@18.2.74): resolution: {integrity: sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -1085,10 +1095,10 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 + '@types/react': 18.2.74 dev: true - /@mui/types@7.2.14(@types/react@18.2.73): + /@mui/types@7.2.14(@types/react@18.2.74): resolution: {integrity: sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -1096,10 +1106,10 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 + '@types/react': 18.2.74 dev: true - /@mui/utils@5.15.14(@types/react@18.2.73)(react@18.2.0): + /@mui/utils@5.15.14(@types/react@18.2.74)(react@18.2.0): resolution: {integrity: sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==} engines: {node: '>=12.0.0'} peerDependencies: @@ -1111,13 +1121,13 @@ packages: dependencies: '@babel/runtime': 7.23.9 '@types/prop-types': 15.7.11 - '@types/react': 18.2.73 + '@types/react': 18.2.74 prop-types: 15.8.1 react: 18.2.0 react-is: 18.2.0 dev: true - /@mui/x-charts@6.19.8(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.14)(@mui/system@5.15.14)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + /@mui/x-charts@6.19.8(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@5.15.14)(@mui/system@5.15.14)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-cjwsCJrUPDlMytJHBV+g3gDoSRURiphjclZs8sRnkZ+h4QbHn24K5QkK4bxEj7aCkO2HVJmDE0aqYEg4BnWCOA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1134,11 +1144,11 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - '@mui/base': 5.0.0-beta.36(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.74)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.74)(react@18.2.0) + '@mui/base': 5.0.0-beta.36(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react@18.2.0) '@react-spring/rafz': 9.7.3 '@react-spring/web': 9.7.3(react-dom@18.2.0)(react@18.2.0) clsx: 2.1.0 @@ -1152,7 +1162,7 @@ packages: - '@types/react' dev: true - /@mui/x-date-pickers@6.19.8(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.14)(@mui/system@5.15.14)(@types/react@18.2.73)(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0): + /@mui/x-date-pickers@6.19.8(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@5.15.14)(@mui/system@5.15.14)(@types/react@18.2.74)(date-fns@2.30.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-6wgc2DoRTR9/mKesku4CVCKr9yYkY3FI2Oy/wshLTs2rFkw2Z10uxXFHBR9ugEtNPNCQv0qqwldElenYI97wsA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1190,12 +1200,12 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - '@mui/base': 5.0.0-beta.36(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react@18.2.0) - '@mui/utils': 5.15.14(@types/react@18.2.73)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.74)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.74)(react@18.2.0) + '@mui/base': 5.0.0-beta.36(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react@18.2.0) + '@mui/utils': 5.15.14(@types/react@18.2.74)(react@18.2.0) '@types/react-transition-group': 4.4.10 clsx: 2.1.0 date-fns: 2.30.0 @@ -1678,7 +1688,7 @@ packages: /@types/hoist-non-react-statics@3.3.5: resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} dependencies: - '@types/react': 18.2.73 + '@types/react': 18.2.74 hoist-non-react-statics: 3.3.2 dev: true @@ -1701,7 +1711,7 @@ packages: /@types/loadable__component@5.13.9: resolution: {integrity: sha512-QWOtIkwZqHNdQj3nixQ8oyihQiTMKZLk/DNuvNxMSbTfxf47w+kqcbnxlUeBgAxdOtW0Dh48dTAIp83iJKtnrQ==} dependencies: - '@types/react': 18.2.73 + '@types/react': 18.2.74 dev: true /@types/lodash-es@4.17.12: @@ -1718,8 +1728,8 @@ packages: resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} dev: true - /@types/node@20.11.30: - resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} + /@types/node@20.12.3: + resolution: {integrity: sha512-sD+ia2ubTeWrOu+YMF+MTAB7E+O7qsMqAbMfW7DG3K1URwhZ5hN1pLlRVGbf4wDFzSfikL05M17EyorS86jShw==} dependencies: undici-types: 5.26.5 dev: true @@ -1739,17 +1749,17 @@ packages: /@types/react-dom@18.2.23: resolution: {integrity: sha512-ZQ71wgGOTmDYpnav2knkjr3qXdAFu0vsk8Ci5w3pGAIdj7/kKAyn+VsQDhXsmzzzepAiI9leWMmubXz690AI/A==} dependencies: - '@types/react': 18.2.73 + '@types/react': 18.2.74 dev: true /@types/react-transition-group@4.4.10: resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} dependencies: - '@types/react': 18.2.73 + '@types/react': 18.2.74 dev: true - /@types/react@18.2.73: - resolution: {integrity: sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==} + /@types/react@18.2.74: + resolution: {integrity: sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==} dependencies: '@types/prop-types': 15.7.11 csstype: 3.1.3 @@ -1770,7 +1780,7 @@ packages: /@types/video-react@0.15.6: resolution: {integrity: sha512-HuqO9ANrpKEz6EiTP8o9qaGy8QEcnM6vQ3DmmTOKsLDSr2YD6jrEyMS6XsWFEQpBOblfIuURpDQcuwC6HdwewQ==} dependencies: - '@types/react': 18.2.73 + '@types/react': 18.2.74 dev: true /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.3): @@ -1971,13 +1981,13 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react-swc@3.6.0(vite@5.2.6): + /@vitejs/plugin-react-swc@3.6.0(vite@5.2.7): resolution: {integrity: sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==} peerDependencies: vite: ^4 || ^5 dependencies: '@swc/core': 1.4.1 - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.7(@types/node@20.12.3) transitivePeerDependencies: - '@swc/helpers' dev: true @@ -4047,7 +4057,7 @@ packages: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true - /mui-markdown@1.1.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.14)(markdown-to-jsx@7.4.5)(react-dom@18.2.0)(react@18.2.0): + /mui-markdown@1.1.13(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@5.15.14)(markdown-to-jsx@7.4.5)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Hlfl89ZGclu1BupsL663ayz+mzEPVNwRf13z7vPWru10Q4IqC0F9K7ZBOkVhvQry1rjFpjr1er/etRqH7HYXuA==} peerDependencies: '@emotion/react': ^11.10.8 @@ -4057,9 +4067,9 @@ packages: react: '>= 17.0.2' react-dom: '>= 17.0.2' dependencies: - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.2.74)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.74)(react@18.2.0) + '@mui/material': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) markdown-to-jsx: 7.4.5(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5200,7 +5210,7 @@ packages: redux: 4.2.1 dev: true - /vite-node@1.4.0(@types/node@20.11.30): + /vite-node@1.4.0(@types/node@20.12.3): resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -5209,7 +5219,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.7(@types/node@20.12.3) transitivePeerDependencies: - '@types/node' - less @@ -5221,7 +5231,7 @@ packages: - terser dev: true - /vite-plugin-html@3.2.2(vite@5.2.6): + /vite-plugin-html@3.2.2(vite@5.2.7): resolution: {integrity: sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==} peerDependencies: vite: '>=2.0.0' @@ -5238,11 +5248,11 @@ packages: html-minifier-terser: 6.1.0 node-html-parser: 5.4.2 pathe: 0.2.0 - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.7(@types/node@20.12.3) dev: true - /vite@5.2.6(@types/node@20.11.30): - resolution: {integrity: sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==} + /vite@5.2.7(@types/node@20.12.3): + resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -5269,7 +5279,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.3 esbuild: 0.20.2 postcss: 8.4.38 rollup: 4.13.0 @@ -5277,7 +5287,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.4.0(@types/node@20.11.30): + /vitest@1.4.0(@types/node@20.12.3): resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -5302,7 +5312,7 @@ packages: jsdom: optional: true dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.3 '@vitest/expect': 1.4.0 '@vitest/runner': 1.4.0 '@vitest/snapshot': 1.4.0 @@ -5320,8 +5330,8 @@ packages: strip-literal: 2.1.0 tinybench: 2.6.0 tinypool: 0.8.3 - vite: 5.2.6(@types/node@20.11.30) - vite-node: 1.4.0(@types/node@20.11.30) + vite: 5.2.7(@types/node@20.12.3) + vite-node: 1.4.0(@types/node@20.12.3) why-is-node-running: 2.2.2 transitivePeerDependencies: - less diff --git a/frontend/src/api/profile.ts b/frontend/src/api/profile.ts index 702a87c37..c2d85542e 100644 --- a/frontend/src/api/profile.ts +++ b/frontend/src/api/profile.ts @@ -231,41 +231,82 @@ export const apiGetNotifications = async ( ); }; -export type PersonConnectionQuery = { - cidr?: string; - source_id?: string; - server_id?: number; - asn?: number; -} & QueryFilter; - -export type PersonConnectionSteamIDQuery = { +export type ConnectionQuery = { + cidr: string; source_id: string; + server_id?: number; + asn: number; } & QueryFilter; export const apiGetConnections = async ( - opts: PersonConnectionQuery, + opts: ConnectionQuery, abortController: AbortController ) => { - const resp = await apiCall< - LazyResult, - PersonConnectionQuery - >(`/api/connections`, 'POST', opts, abortController); + const resp = await apiCall, ConnectionQuery>( + `/api/connections`, + 'POST', + opts, + abortController + ); resp.data = resp.data.map(transformCreatedOnDate); return resp; }; -export const apiGetConnectionsSteam = async ( - opts: PersonConnectionSteamIDQuery, +export type IPQuery = { + ip: string; +}; + +export type NetworkLocation = { + cidr: string; + country_code: string; + country_name: string; + region_name: string; + city_name: string; + lat_long: { + latitude: number; + longitude: number; + }; +}; + +export type NetworkASN = { + cidr: string; + as_num: number; + as_name: string; +}; + +export type NetworkProxy = { + cidr: string; + proxy_type: string; + country_code: string; + country_name: string; + region_name: string; + city_name: string; + isp: string; + domain: string; + usage_type: string; + asn: number; + as: string; + last_seen: string; + threat: string; +}; + +export type NetworkDetails = { + location: NetworkLocation; + asn: NetworkASN; + proxy: NetworkProxy; +}; + +export const apiGetNetworkDetails = async ( + opts: IPQuery, abortController: AbortController ) => { - const resp = await apiCall< - LazyResult, - PersonConnectionQuery - >(`/api/connections/steam`, 'POST', opts, abortController); - - resp.data = resp.data.map(transformCreatedOnDate); - return resp; + return await apiCall( + `/api/network`, + 'POST', + opts, + abortController + ); }; interface PermissionUpdate { diff --git a/frontend/src/component/FindPlayerByIP.tsx b/frontend/src/component/FindPlayerByIP.tsx new file mode 100644 index 000000000..83d4936d2 --- /dev/null +++ b/frontend/src/component/FindPlayerByIP.tsx @@ -0,0 +1,171 @@ +import { ChangeEvent, useState } from 'react'; +import SearchIcon from '@mui/icons-material/Search'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Formik } from 'formik'; +import { PersonConnection } from '../api'; +import { useConnections } from '../hooks/useConnections.ts'; +import { Order, RowsPerPage } from '../util/table.ts'; +import { renderDateTime } from '../util/text.tsx'; +import { LoadingPlaceholder } from './LoadingPlaceholder.tsx'; +import { + CIDRInputFieldProps, + NetworkRangeField +} from './formik/NetworkRangeField.tsx'; +import { SubmitButton } from './modal/Buttons.tsx'; +import { LazyTable } from './table/LazyTable.tsx'; + +export const FindPlayerByIP = () => { + const [sortOrder, setSortOrder] = useState('desc'); + const [sortColumn, setSortColumn] = + useState('created_on'); + const [rowPerPageCount, setRowPerPageCount] = useState( + RowsPerPage.Ten + ); + const [cidr, setCIDR] = useState(''); + const [page, setPage] = useState(0); + + const { + data: rows, + count, + loading + } = useConnections({ + limit: rowPerPageCount, + offset: page * rowPerPageCount, + desc: sortOrder == 'desc', + order_by: sortColumn, + cidr: cidr, + asn: 0, + source_id: '' + }); + + const onSubmit = (values: CIDRInputFieldProps) => { + setCIDR(values.cidr); + }; + + return ( + + + + + + + + + } + /> + + + + + + {loading ? ( + + ) : ( + + showPager={true} + count={count} + rows={rows} + page={page} + rowsPerPage={rowPerPageCount} + sortOrder={sortOrder} + sortColumn={sortColumn} + onSortColumnChanged={async (column) => { + setSortColumn(column); + }} + onSortOrderChanged={async (direction) => { + setSortOrder(direction); + }} + onPageChange={(_, newPage: number) => { + setPage(newPage); + }} + onRowsPerPageChange={( + event: ChangeEvent< + HTMLInputElement | HTMLTextAreaElement + > + ) => { + setRowPerPageCount( + parseInt(event.target.value, 10) + ); + setPage(0); + }} + columns={[ + { + label: 'Created', + tooltip: 'Created On', + sortKey: 'created_on', + sortType: 'date', + align: 'left', + width: '150px', + sortable: true, + renderer: (obj: PersonConnection) => ( + + {renderDateTime(obj.created_on)} + + ) + }, + { + label: 'Name', + tooltip: 'Name', + sortKey: 'persona_name', + sortType: 'string', + align: 'left', + width: '200px', + sortable: true + }, + { + label: 'SteamID', + tooltip: 'Name', + sortKey: 'steam_id', + sortType: 'string', + align: 'left', + width: '200px', + sortable: true + }, + { + label: 'IP Address', + tooltip: 'IP Address', + sortKey: 'ip_addr', + sortType: 'string', + align: 'left', + width: '150px', + sortable: true + }, + { + label: 'Server', + tooltip: 'IP Address', + sortKey: 'ip_addr', + sortType: 'string', + align: 'left', + sortable: true, + renderer: (obj: PersonConnection) => { + return ( + + + {obj.server_name_short ?? + 'Unknown'} + + + ); + } + } + ]} + /> + )} + + + ); +}; diff --git a/frontend/src/component/FindPlayerIPs.tsx b/frontend/src/component/FindPlayerIPs.tsx new file mode 100644 index 000000000..13e7940ee --- /dev/null +++ b/frontend/src/component/FindPlayerIPs.tsx @@ -0,0 +1,104 @@ +import { ChangeEvent, useState } from 'react'; +import SearchIcon from '@mui/icons-material/Search'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Formik } from 'formik'; +import { PersonConnection } from '../api'; +import { useConnections } from '../hooks/useConnections.ts'; +import { Order, RowsPerPage } from '../util/table.ts'; +import { LoadingPlaceholder } from './LoadingPlaceholder.tsx'; +import { TargetIDField, TargetIDInputValue } from './formik/TargetIdField.tsx'; +import { SubmitButton } from './modal/Buttons.tsx'; +import { LazyTable } from './table/LazyTable.tsx'; +import { connectionColumns } from './table/connectionColumns.tsx'; + +export const FindPlayerIPs = () => { + const [sortOrder, setSortOrder] = useState('desc'); + const [sortColumn, setSortColumn] = + useState('created_on'); + const [rowPerPageCount, setRowPerPageCount] = useState( + RowsPerPage.Ten + ); + const [steamID, setSteamID] = useState(''); + const [page, setPage] = useState(0); + + const { + data: rows, + count, + loading + } = useConnections({ + limit: rowPerPageCount, + offset: page * rowPerPageCount, + desc: sortOrder == 'desc', + order_by: sortColumn, + source_id: steamID, + asn: 0, + cidr: '' + }); + + const onSubmit = (values: TargetIDInputValue) => { + setSteamID(values.target_id); + }; + + return ( + + + + + + + + + } + /> + + + + + + {loading ? ( + + ) : ( + + showPager={true} + count={count} + rows={rows} + page={page} + rowsPerPage={rowPerPageCount} + sortOrder={sortOrder} + sortColumn={sortColumn} + onSortColumnChanged={async (column) => { + setSortColumn(column); + }} + onSortOrderChanged={async (direction) => { + setSortOrder(direction); + }} + onPageChange={(_, newPage: number) => { + setPage(newPage); + }} + onRowsPerPageChange={( + event: ChangeEvent< + HTMLInputElement | HTMLTextAreaElement + > + ) => { + setRowPerPageCount( + parseInt(event.target.value, 10) + ); + setPage(0); + }} + columns={connectionColumns} + /> + )} + + + ); +}; diff --git a/frontend/src/component/NetworkInfo.tsx b/frontend/src/component/NetworkInfo.tsx new file mode 100644 index 000000000..3ed8948dd --- /dev/null +++ b/frontend/src/component/NetworkInfo.tsx @@ -0,0 +1,232 @@ +import { ReactNode, useMemo, useState } from 'react'; +import { MapContainer, Marker, TileLayer } from 'react-leaflet'; +import SearchIcon from '@mui/icons-material/Search'; +import Link from '@mui/material/Link'; +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 TableRow from '@mui/material/TableRow'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Formik } from 'formik'; +import 'leaflet/dist/leaflet.css'; +import { useNetworkQuery } from '../hooks/useNetworkQuery.ts'; +import { getFlagEmoji } from '../util/emoji.ts'; +import { LoadingPlaceholder } from './LoadingPlaceholder.tsx'; +import { IPField, IPFieldProps } from './formik/IPField.tsx'; +import { SubmitButton } from './modal/Buttons.tsx'; + +const InfoRow = ({ + label, + children +}: { + label: string; + children: ReactNode; +}) => { + return ( + + + {label} + + {children} + + ); +}; + +export const NetworkInfo = () => { + const [ip, setIP] = useState(''); + + const { data, loading } = useNetworkQuery({ ip: ip }); + + const onSubmit = (values: IPFieldProps) => { + setIP(values.ip); + }; + + const pos = useMemo(() => { + if (!data || data?.location.lat_long.latitude == 0) { + return { lat: 50, lng: 50 }; + } + return { + lat: data?.location.lat_long.latitude, + lng: data?.location.lat_long.longitude + }; + }, [data]); + + return ( + <> + + + + + + + + + } + /> + + + + + + + + {loading ? ( + + ) : ( +
+ + + + Location + + + + + + {data && ( + <> + {data?.location + .country_code && + getFlagEmoji( + data + ?.location + .country_code + )}{' '} + { + data?.location + .country_code + }{' '} + ( + { + data?.location + .country_code + } + ) + + )} + + + {data?.location.region_name} + + + {data?.location.city_name} + + + { + data?.location.lat_long + .latitude + } + + + { + data?.location.lat_long + .longitude + } + + +
+
+
+ + + + {(data?.location.lat_long.latitude || + 0) > 0 && ( + + )} + + + + + ASN + + + + + + {data?.asn.as_name} + + + + {data?.asn.as_num} + + + + {data?.asn.cidr} + + +
+
+
+ + + Proxy Info + + + + + + {data?.proxy.proxy_type} + + + {data?.proxy.isp} + + + {data?.proxy.domain} + + + {data?.proxy.usage_type} + + + {data?.proxy.last_seen} + + + {data?.proxy.threat} + + +
+
+
+
+
+ )} +
+
+ + ); +}; diff --git a/frontend/src/component/formik/IPField.tsx b/frontend/src/component/formik/IPField.tsx index 78f24f69b..9fe7cd527 100644 --- a/frontend/src/component/formik/IPField.tsx +++ b/frontend/src/component/formik/IPField.tsx @@ -1,14 +1,13 @@ import TextField from '@mui/material/TextField'; import { useFormikContext } from 'formik'; -interface IPFieldProps { +export type IPFieldProps = { ip: string; -} +}; -export const IPField = () => { - const { values, touched, errors, handleChange } = useFormikContext< - T & IPFieldProps - >(); +export const IPField = () => { + const { values, touched, errors, handleChange } = + useFormikContext(); return ( { useEffect(() => { const abortController = new AbortController(); - const opts: PersonConnectionQuery = { + const opts: ConnectionQuery = { limit: rowPerPageCount, offset: page * rowPerPageCount, order_by: sortColumn, diff --git a/frontend/src/hooks/useConnectionsBySteamID.ts b/frontend/src/hooks/useConnections.ts similarity index 65% rename from frontend/src/hooks/useConnectionsBySteamID.ts rename to frontend/src/hooks/useConnections.ts index a599751f9..cdd0489cb 100644 --- a/frontend/src/hooks/useConnectionsBySteamID.ts +++ b/frontend/src/hooks/useConnections.ts @@ -1,23 +1,24 @@ import { useEffect, useState } from 'react'; -import { - apiGetConnectionsSteam, - PersonConnection, - PersonConnectionSteamIDQuery -} from '../api'; +import { apiGetConnections, PersonConnection, ConnectionQuery } from '../api'; import { logErr } from '../util/errors'; -export const useConnectionsBySteamID = (opts: PersonConnectionSteamIDQuery) => { +export const useConnections = (opts: ConnectionQuery) => { const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [count, setCount] = useState(0); useEffect(() => { + if (!(opts.cidr != '' || opts.asn > 0 || opts.source_id != '')) { + return; + } const abortController = new AbortController(); setLoading(true); - apiGetConnectionsSteam( + apiGetConnections( { + cidr: opts.cidr, source_id: opts.source_id, + asn: opts.asn, desc: opts.desc, order_by: opts.order_by, limit: opts.limit, @@ -35,7 +36,15 @@ export const useConnectionsBySteamID = (opts: PersonConnectionSteamIDQuery) => { }); return () => abortController.abort(); - }, [opts.desc, opts.limit, opts.offset, opts.order_by, opts.source_id]); + }, [ + opts.desc, + opts.limit, + opts.offset, + opts.order_by, + opts.cidr, + opts.asn, + opts.source_id + ]); return { data, count, loading }; }; diff --git a/frontend/src/hooks/useNetworkQuery.ts b/frontend/src/hooks/useNetworkQuery.ts new file mode 100644 index 000000000..c304cea62 --- /dev/null +++ b/frontend/src/hooks/useNetworkQuery.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from 'react'; +import { apiGetNetworkDetails, IPQuery, NetworkDetails } from '../api'; +import { logErr } from '../util/errors'; + +export const useNetworkQuery = (opts: IPQuery) => { + const [loading, setLoading] = useState(false); + const [data, setData] = useState(); + + useEffect(() => { + if (opts.ip == '') { + return; + } + const abortController = new AbortController(); + + setLoading(true); + apiGetNetworkDetails({ ip: opts.ip }, abortController) + .then((resp) => { + setData(resp); + }) + .catch(logErr) + .finally(() => { + setLoading(false); + }); + + return () => abortController.abort(); + }, [opts.ip]); + + return { data, loading }; +}; diff --git a/frontend/src/page/AdminNetworkPage.tsx b/frontend/src/page/AdminNetworkPage.tsx index 7ceec1a28..045bcc36c 100644 --- a/frontend/src/page/AdminNetworkPage.tsx +++ b/frontend/src/page/AdminNetworkPage.tsx @@ -1,7 +1,6 @@ -import { ChangeEvent, SyntheticEvent, useCallback, useState } from 'react'; +import { SyntheticEvent, useState } from 'react'; import HelpIcon from '@mui/icons-material/Help'; import LeakAddIcon from '@mui/icons-material/LeakAdd'; -import SearchIcon from '@mui/icons-material/Search'; import VpnLockIcon from '@mui/icons-material/VpnLock'; import Box from '@mui/material/Box'; import List from '@mui/material/List'; @@ -10,189 +9,14 @@ import ListItemText from '@mui/material/ListItemText'; import Stack from '@mui/material/Stack'; import Tab from '@mui/material/Tab'; import Tabs from '@mui/material/Tabs'; -import TextField from '@mui/material/TextField'; import Grid from '@mui/material/Unstable_Grid2'; -import { Formik } from 'formik'; -import IPCIDR from 'ip-cidr'; -import { PersonConnection } from '../api'; import { ContainerWithHeader } from '../component/ContainerWithHeader'; -import { LoadingPlaceholder } from '../component/LoadingPlaceholder.tsx'; +import { FindPlayerByIP } from '../component/FindPlayerByIP.tsx'; +import { FindPlayerIPs } from '../component/FindPlayerIPs.tsx'; import { NetworkBlockChecker } from '../component/NetworkBlockChecker'; import { NetworkBlockSources } from '../component/NetworkBlockSources'; +import { NetworkInfo } from '../component/NetworkInfo.tsx'; import { TabPanel } from '../component/TabPanel'; -import { - TargetIDField, - TargetIDInputValue -} from '../component/formik/TargetIdField.tsx'; -import { SubmitButton } from '../component/modal/Buttons.tsx'; -import { LazyTable } from '../component/table/LazyTable.tsx'; -import { connectionColumns } from '../component/table/connectionColumns.tsx'; -import { useConnectionsBySteamID } from '../hooks/useConnectionsBySteamID.ts'; -import { Order, RowsPerPage } from '../util/table.ts'; - -interface NetworkInputProps { - onValidChange: (cidr: string) => void; -} - -export const NetworkInput = ({ onValidChange }: NetworkInputProps) => { - const defaultHelperText = 'Enter a IP address or CIDR range'; - const [error, setError] = useState(''); - const [value, setValue] = useState(''); - const [helper, setHelper] = useState(defaultHelperText); - - const onChange = useCallback( - (evt: ChangeEvent) => { - const address = evt.target.value; - if (address == '') { - setError(''); - setValue(address); - setHelper(defaultHelperText); - return; - } - if (!address.match(`^([0-9./]+?)$`)) { - return; - } - - setValue(address); - - if (address.length > 0 && !IPCIDR.isValidAddress(address)) { - setError('Invalid address'); - return; - } - - setError(''); - - try { - const cidr = new IPCIDR(address); - setHelper(`Total hosts in range: ${cidr.size}`); - onValidChange(address); - } catch (e) { - if (IPCIDR.isValidAddress(address)) { - setHelper(`Total hosts in range: 1`); - onValidChange(address); - } - return; - } - }, - [onValidChange] - ); - - return ( - - ); -}; - -const FindPlayerByIP = () => { - return ( - - - { - console.log(cidr); - }} - /> - - - ); -}; - -const FindPlayerIPs = () => { - const [sortOrder, setSortOrder] = useState('desc'); - const [sortColumn, setSortColumn] = - useState('created_on'); - const [rowPerPageCount, setRowPerPageCount] = useState( - RowsPerPage.Ten - ); - const [steamID, setSteamID] = useState(''); - const [page, setPage] = useState(0); - - const { - data: rows, - count, - loading - } = useConnectionsBySteamID({ - limit: rowPerPageCount, - offset: page, - desc: sortOrder == 'desc', - order_by: sortColumn, - source_id: steamID - }); - - const onSubmit = (values: TargetIDInputValue) => { - setSteamID(values.target_id); - }; - - return ( - - - - - - - - - } - /> - - - - - - {loading ? ( - - ) : ( - - showPager={true} - count={count} - rows={rows} - page={page} - rowsPerPage={rowPerPageCount} - sortOrder={sortOrder} - sortColumn={sortColumn} - onSortColumnChanged={async (column) => { - setSortColumn(column); - }} - onSortOrderChanged={async (direction) => { - setSortOrder(direction); - }} - onPageChange={(_, newPage: number) => { - setPage(newPage); - }} - onRowsPerPageChange={( - event: ChangeEvent< - HTMLInputElement | HTMLTextAreaElement - > - ) => { - setRowPerPageCount( - parseInt(event.target.value, 10) - ); - setPage(0); - }} - columns={connectionColumns} - /> - )} - - - ); -}; export const AdminNetworkPage = () => { const [value, setValue] = useState(0); @@ -227,7 +51,7 @@ export const AdminNetworkPage = () => { - IPInfo + diff --git a/frontend/src/util/emoji.ts b/frontend/src/util/emoji.ts new file mode 100644 index 000000000..ffe7260da --- /dev/null +++ b/frontend/src/util/emoji.ts @@ -0,0 +1,7 @@ +export const getFlagEmoji = (countryCode) => { + const codePoints = countryCode + .toUpperCase() + .split('') + .map((char) => 127397 + char.charCodeAt()); + return String.fromCodePoint(...codePoints); +}; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 782fa21fd..439f75c13 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -25,14 +25,15 @@ export default defineConfig({ open: true, port: 6007, cors: true, + host: 'gbans.localhost', proxy: { '/auth/callback': { - target: 'http://localhost:6006', + target: 'http://gbans.localhost:6006', changeOrigin: true, secure: false }, '/api': { - target: 'http://localhost:6006', + target: 'http://gbans.localhost:6006', changeOrigin: true, secure: false } diff --git a/go.mod b/go.mod index 5b8fcb461..8ef7daba3 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.1 require ( github.com/Depado/ginprom v1.8.1 github.com/Masterminds/squirrel v1.5.4 - github.com/bwmarrin/discordgo v0.27.1 + github.com/bwmarrin/discordgo v0.28.1 github.com/dotse/slug v0.1.0 github.com/gabriel-vasile/mimetype v1.4.3 github.com/getsentry/sentry-go v0.27.0 diff --git a/go.sum b/go.sum index aae591d8f..d047aa70e 100644 --- a/go.sum +++ b/go.sum @@ -421,12 +421,15 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= +github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA= github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -718,6 +721,7 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= @@ -740,6 +744,7 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= @@ -774,6 +779,7 @@ github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiw github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= @@ -911,6 +917,7 @@ github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/ github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= diff --git a/internal/ban/ban_asn_usecase.go b/internal/ban/ban_asn_usecase.go index 9c626fb5d..5241fc65d 100644 --- a/internal/ban/ban_asn_usecase.go +++ b/internal/ban/ban_asn_usecase.go @@ -57,16 +57,7 @@ func (s banASNUsecase) Unban(ctx context.Context, asnNum string) (bool, error) { return false, errors.Join(errDrop, domain.ErrDropASNBan) } - asnNetworks, errGetASNRecords := s.networkUsecase.GetASNRecordsByNum(ctx, asNum) - if errGetASNRecords != nil { - if errors.Is(errGetASNRecords, domain.ErrNoResult) { - return false, errors.Join(errGetASNRecords, domain.ErrUnknownASN) - } - - return false, errors.Join(errGetASNRecords, domain.ErrFetchASN) - } - - s.discordUsecase.SendPayload(domain.ChannelModLog, discord.UnbanASNMessage(asNum, asnNetworks)) + s.discordUsecase.SendPayload(domain.ChannelModLog, discord.UnbanASNMessage(asNum)) return true, nil } diff --git a/internal/ban/ban_net_repository.go b/internal/ban/ban_net_repository.go index b583edaa4..194a30a00 100644 --- a/internal/ban/ban_net_repository.go +++ b/internal/ban/ban_net_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net" + "net/netip" "time" sq "github.com/Masterminds/squirrel" @@ -25,7 +26,7 @@ func NewBanNetRepository(database database.Database) domain.BanNetRepository { // // Note that this function does not currently limit results returned. This may change in the future, do not // rely on this functionality. -func (r banNetRepository) GetByAddress(ctx context.Context, ipAddr net.IP) ([]domain.BanCIDR, error) { +func (r banNetRepository) GetByAddress(ctx context.Context, ipAddr netip.Addr) ([]domain.BanCIDR, error) { const query = ` SELECT net_id, cidr, origin, created_on, updated_on, reason, reason_text, valid_until, deleted, note, unban_reason_text, is_enabled, target_id, source_id, appeal_state @@ -124,7 +125,7 @@ func (r banNetRepository) Get(ctx context.Context, filter domain.CIDRBansQueryFi if errCidr != nil { ip := net.ParseIP(filter.IP) if ip == nil { - return nil, 0, errors.Join(errCidr, domain.ErrInvalidIP) + return nil, 0, errors.Join(errCidr, domain.ErrNetworkInvalidIP) } addr = ip.String() diff --git a/internal/ban/ban_net_usecase.go b/internal/ban/ban_net_usecase.go index bf834ef64..6c71d6da3 100644 --- a/internal/ban/ban_net_usecase.go +++ b/internal/ban/ban_net_usecase.go @@ -5,6 +5,7 @@ import ( "errors" "log/slog" "net" + "net/netip" "github.com/leighmacdonald/gbans/internal/discord" "github.com/leighmacdonald/gbans/internal/domain" @@ -48,7 +49,7 @@ func (s *banNetUsecase) Ban(ctx context.Context, banNet *domain.BanCIDR) error { _, realCIDR, errCIDR := net.ParseCIDR(banNet.CIDR) if errCIDR != nil { - return errors.Join(errCIDR, domain.ErrInvalidIP) + return errors.Join(errCIDR, domain.ErrNetworkInvalidIP) } if errSaveBanNet := s.banRepo.Save(ctx, banNet); errSaveBanNet != nil { @@ -86,7 +87,7 @@ func (s *banNetUsecase) Ban(ctx context.Context, banNet *domain.BanCIDR) error { return nil } -func (s *banNetUsecase) GetByAddress(ctx context.Context, ipAddr net.IP) ([]domain.BanCIDR, error) { +func (s *banNetUsecase) GetByAddress(ctx context.Context, ipAddr netip.Addr) ([]domain.BanCIDR, error) { return s.banRepo.GetByAddress(ctx, ipAddr) } diff --git a/internal/ban/ban_steam_repository.go b/internal/ban/ban_steam_repository.go index 9fc185223..5a79198fd 100644 --- a/internal/ban/ban_steam_repository.go +++ b/internal/ban/ban_steam_repository.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "net" + "net/netip" "time" sq "github.com/Masterminds/squirrel" @@ -128,8 +128,9 @@ func (r *banSteamRepository) GetByBanID(ctx context.Context, banID int64, delete return r.getBanByColumn(ctx, "ban_id", banID, deletedOk) } -func (r *banSteamRepository) GetByLastIP(ctx context.Context, lastIP net.IP, deletedOk bool) (domain.BannedSteamPerson, error) { - return r.getBanByColumn(ctx, "last_ip", fmt.Sprintf("::ffff:%s", lastIP.String()), deletedOk) +func (r *banSteamRepository) GetByLastIP(ctx context.Context, lastIP netip.Addr, deletedOk bool) (domain.BannedSteamPerson, error) { + // TODO check if works still + return r.getBanByColumn(ctx, "last_ip", lastIP.String(), deletedOk) } // Save will insert or update the ban record diff --git a/internal/ban/ban_steam_usecase.go b/internal/ban/ban_steam_usecase.go index c22f43c7d..71e985a06 100644 --- a/internal/ban/ban_steam_usecase.go +++ b/internal/ban/ban_steam_usecase.go @@ -4,7 +4,7 @@ import ( "context" "errors" "log/slog" - "net" + "net/netip" "time" "github.com/leighmacdonald/gbans/internal/discord" @@ -52,7 +52,7 @@ func (s *banSteamUsecase) GetByBanID(ctx context.Context, banID int64, deletedOk return s.banRepo.GetByBanID(ctx, banID, deletedOk) } -func (s *banSteamUsecase) GetByLastIP(ctx context.Context, lastIP net.IP, deletedOk bool) (domain.BannedSteamPerson, error) { +func (s *banSteamUsecase) GetByLastIP(ctx context.Context, lastIP netip.Addr, deletedOk bool) (domain.BannedSteamPerson, error) { return s.banRepo.GetByLastIP(ctx, lastIP, deletedOk) } @@ -178,7 +178,7 @@ func (s *banSteamUsecase) GetOlderThan(ctx context.Context, filter domain.QueryF // IsOnIPWithBan checks if the address matches an existing user who is currently banned already. This // function will always fail-open and allow players in if an error occurs. -func (s *banSteamUsecase) IsOnIPWithBan(ctx context.Context, curUser domain.PersonInfo, steamID steamid.SteamID, address net.IP) (bool, error) { +func (s *banSteamUsecase) IsOnIPWithBan(ctx context.Context, curUser domain.PersonInfo, steamID steamid.SteamID, address netip.Addr) (bool, error) { existing, errMatch := s.GetByLastIP(ctx, address, false) if errMatch != nil { if errors.Is(errMatch, domain.ErrNoResult) { diff --git a/internal/blocklist/blocklist_service.go b/internal/blocklist/blocklist_service.go index 084c02149..0eb76e566 100644 --- a/internal/blocklist/blocklist_service.go +++ b/internal/blocklist/blocklist_service.go @@ -2,8 +2,8 @@ package blocklist import ( "log/slog" - "net" "net/http" + "net/netip" "github.com/gin-gonic/gin" "github.com/leighmacdonald/gbans/internal/domain" @@ -253,7 +253,7 @@ func (b *blocklistHandler) onAPIDeleteBlockListWhitelist() gin.HandlerFunc { func (b *blocklistHandler) onAPIPostBlocklistCheck() gin.HandlerFunc { type checkReq struct { - Address string `json:"address"` + Address netip.Addr `json:"address"` } type checkResp struct { @@ -267,14 +267,7 @@ func (b *blocklistHandler) onAPIPostBlocklistCheck() gin.HandlerFunc { return } - ipAddr := net.ParseIP(req.Address) - if ipAddr == nil { - httphelper.ResponseErr(ctx, http.StatusBadRequest, domain.ErrBadRequest) - - return - } - - source, isBlocked := b.nu.IsMatch(ipAddr) + source, isBlocked := b.nu.IsMatch(req.Address) ctx.JSON(http.StatusOK, checkResp{ Blocked: isBlocked, diff --git a/internal/database/migrations/000081_ip4r.down.sql b/internal/database/migrations/000081_ip4r.down.sql new file mode 100644 index 000000000..0ea4fb129 --- /dev/null +++ b/internal/database/migrations/000081_ip4r.down.sql @@ -0,0 +1,21 @@ +BEGIN; + +-- Add a temp column to map to the new type +ALTER TABLE person_connections ADD COLUMN new_inet inet; + +-- Load our new data type, its using "dual"/128bit format, so we just trim the prefix as we +-- don't actually care about ipv6 since its not supported anyways. +UPDATE person_connections set new_inet = ip_addr::inet where (1=1); + +-- Remove old column +ALTER TABLE person_connections DROP COLUMN ip_addr; + +-- Replace with our new column +ALTER TABLE person_connections RENAME COLUMN new_inet TO ip_addr; + +-- Add index +DROP INDEX ip_addr_idx; + +DROP EXTENSION ip4r; + +COMMIT; diff --git a/internal/database/migrations/000081_ip4r.up.sql b/internal/database/migrations/000081_ip4r.up.sql new file mode 100644 index 000000000..3fc541dd8 --- /dev/null +++ b/internal/database/migrations/000081_ip4r.up.sql @@ -0,0 +1,47 @@ +BEGIN; + +-- Remove our custom iprange type, ip4r has its own +ALTER TABLE net_asn DROP column ip_range; +ALTER TABLE net_location DROP column ip_range; +ALTER TABLE net_proxy DROP column ip_range; + +DROP TYPE iprange; + +-- Install extension +CREATE EXTENSION ip4r; + +-- Clean out the tables since we dont have to worry about these and can just re-import. +TRUNCATE net_asn; +TRUNCATE net_location; +TRUNCATE net_proxy; + +-- Replace ip_range with the ip4r type +ALTER TABLE net_asn ADD COLUMN ip_range ip4r not null; +ALTER TABLE net_location ADD COLUMN ip_range ip4r not null; +ALTER TABLE net_proxy ADD COLUMN ip_range ip4r not null; + +ALTER TABLE net_proxy DROP COLUMN ip_from; +ALTER TABLE net_proxy DROP COLUMN ip_to; +ALTER TABLE net_location DROP COLUMN ip_from; +ALTER TABLE net_location DROP COLUMN ip_to; +ALTER TABLE net_asn DROP COLUMN ip_from; +ALTER TABLE net_asn DROP COLUMN ip_to; + + +-- Add a temp column to map to the new type +ALTER TABLE person_connections ADD COLUMN new_ip4 ip4; + +-- Load our new data type, its using "dual"/128bit format, so we just trim the prefix as we +-- don't actually care about ipv6 since its not supported anyways. +UPDATE person_connections set new_ip4 = TRIM('::ffff:' from abbrev(ip_addr))::ip4 where (1=1); + +-- Remove old column +ALTER TABLE person_connections DROP COLUMN ip_addr; + +-- Replace with our new column +ALTER TABLE person_connections RENAME COLUMN new_ip4 TO ip_addr; + +-- Add index +CREATE INDEX ip_addr_idx ON person_connections (ip_addr); + +COMMIT; diff --git a/internal/discord/discord_service.go b/internal/discord/discord_service.go index 34257ed3c..40f01fbf5 100644 --- a/internal/discord/discord_service.go +++ b/internal/discord/discord_service.go @@ -6,17 +6,16 @@ import ( "fmt" "log/slog" "net" + "net/netip" "sort" "strconv" "strings" - "sync" "time" "github.com/bwmarrin/discordgo" "github.com/gofrs/uuid/v5" "github.com/leighmacdonald/gbans/internal/domain" "github.com/leighmacdonald/gbans/internal/thirdparty" - "github.com/leighmacdonald/gbans/pkg/ip2location" "github.com/leighmacdonald/gbans/pkg/log" "github.com/leighmacdonald/gbans/pkg/util" "github.com/leighmacdonald/steamid/v4/steamid" @@ -211,48 +210,12 @@ func (h discordService) makeOnCheck() func(_ context.Context, _ *discordgo.Sessi slog.Warn("Failed to fetch logTF data", log.ErrAttr(errLogs)) } - var ( - waitGroup = &sync.WaitGroup{} - asn ip2location.ASNRecord - location ip2location.LocationRecord - proxy ip2location.ProxyRecord - ) - - waitGroup.Add(3) - - go func() { - defer waitGroup.Done() - - if player.IPAddr != nil { - if errASN := h.nu.GetASNRecordByIP(ctx, player.IPAddr, &asn); errASN != nil { - slog.Error("Failed to fetch ASN record", log.ErrAttr(errASN)) - } - } - }() - - go func() { - defer waitGroup.Done() - - if player.IPAddr != nil { - if errLoc := h.nu.GetLocationRecord(ctx, player.IPAddr, &location); errLoc != nil { - slog.Error("Failed to fetch Location record", log.ErrAttr(errLoc)) - } - } - }() - - go func() { - defer waitGroup.Done() - - if player.IPAddr != nil { - if errProxy := h.nu.GetProxyRecord(ctx, player.IPAddr, &proxy); errProxy != nil && !errors.Is(errProxy, domain.ErrNoResult) { - slog.Error("Failed to fetch proxy record", log.ErrAttr(errProxy)) - } - } - }() - - waitGroup.Wait() + network, errNetwork := h.nu.QueryNetwork(ctx, player.IPAddr) + if errNetwork != nil { + slog.Error("Failed to query network details") + } - return CheckMessage(player, ban, banURL, authorProfile, oldBans, bannedNets, asn, location, proxy, logData), nil + return CheckMessage(player, ban, banURL, authorProfile, oldBans, bannedNets, network.Asn, network.Location, network.Proxy, logData), nil } } @@ -281,22 +244,7 @@ func (h discordService) onHistoryIP(ctx context.Context, _ *discordgo.Session, i return nil, domain.ErrCommandFailed } - ipRecords, errGetPersonIPHist := h.nu.GetPersonIPHistory(ctx, steamID, 20) - if errGetPersonIPHist != nil && !errors.Is(errGetPersonIPHist, domain.ErrNoResult) { - return nil, domain.ErrCommandFailed - } - - lastIP := net.IP{} - - for _, ipRecord := range ipRecords { - // TODO Join query for connections and geoip lookup data - // addField(embed, ipRecord.IPAddr.String(), fmt.Sprintf("%s %s %s %s %s %s %s %s", Config.FmtTimeShort(ipRecord.CreatedOn), ipRecord.CC, - // ipRecord.CityName, ipRecord.ASName, ipRecord.ISP, ipRecord.UsageType, ipRecord.Threat, ipRecord.DomainUsed)) - // lastIP = ipRecord.IPAddr - if ipRecord.IPAddr.Equal(lastIP) { - continue - } - } + // TODO actually show record return HistoryMessage(person), nil } @@ -395,16 +343,7 @@ func (h discordService) onUnbanASN(ctx context.Context, _ *discordgo.Session, in return nil, domain.ErrParseASN } - asnNetworks, errGetASNRecords := h.nu.GetASNRecordsByNum(ctx, asNum) - if errGetASNRecords != nil { - if errors.Is(errGetASNRecords, domain.ErrNoResult) { - return nil, domain.ErrFetchASN - } - - return nil, domain.ErrFetchASN - } - - return UnbanASNMessage(asNum, asnNetworks), nil + return UnbanASNMessage(asNum), nil } func (h discordService) getDiscordAuthor(ctx context.Context, interaction *discordgo.InteractionCreate) (domain.Person, error) { @@ -536,32 +475,33 @@ func (h discordService) makeOnPlayers() func(context.Context, *discordgo.Session }) for _, player := range serverState.Players { - var asn ip2location.ASNRecord - if errASN := h.nu.GetASNRecordByIP(ctx, player.IP, &asn); errASN != nil { - // Will fail for LAN ips - slog.Warn("Failed to get asn record", log.ErrAttr(errASN)) - } + address, errIP := netip.ParseAddr(player.IP.String()) + if errIP != nil { + slog.Error("Failed to parse player ip", log.ErrAttr(errIP)) - var loc ip2location.LocationRecord - if errLoc := h.nu.GetLocationRecord(ctx, player.IP, &loc); errLoc != nil { - slog.Warn("Failed to get location record: %v", log.ErrAttr(errLoc)) + continue } - proxyStr := "" + network, errNetwork := h.nu.QueryNetwork(ctx, address) + if errNetwork != nil { + slog.Error("Failed to get network info", log.ErrAttr(errNetwork)) - var proxy ip2location.ProxyRecord - if errGetProxyRecord := h.nu.GetProxyRecord(ctx, player.IP, &proxy); errGetProxyRecord == nil { - proxyStr = fmt.Sprintf("Threat: %s | %s | %s", proxy.ProxyType, proxy.Threat, proxy.UsageType) + continue } flag := "" - if loc.CountryCode != "" { - flag = fmt.Sprintf(":flag_%s: ", strings.ToLower(loc.CountryCode)) + if network.Location.CountryCode != "" { + flag = fmt.Sprintf(":flag_%s: ", strings.ToLower(network.Location.CountryCode)) } asStr := "" - if asn.ASNum > 0 { - asStr = fmt.Sprintf("[ASN](https://spyse.com/target/as/%d) ", asn.ASNum) + if network.Asn.ASNum > 0 { + asStr = fmt.Sprintf("[ASN](https://spyse.com/target/as/%d) ", network.Asn.ASNum) + } + + proxyStr := "" + if network.Proxy.ProxyType != "" { + proxyStr = fmt.Sprintf("Threat: %s | %s | %s", network.Proxy.ProxyType, network.Proxy.Threat, network.Proxy.UsageType) } rows = append(rows, fmt.Sprintf("%s`%s` %s`%3dms` [%s](https://steamcommunity.com/profiles/%s)%s", @@ -896,15 +836,6 @@ func (h discordService) onBanASN(ctx context.Context, _ *discordgo.Session, return nil, domain.ErrParseASN } - asnRecords, errGetASNRecords := h.nu.GetASNRecordsByNum(ctx, asNum) - if errGetASNRecords != nil { - if errors.Is(errGetASNRecords, domain.ErrNoResult) { - return nil, domain.ErrASNNoRecords - } - - return nil, domain.ErrFetchASN - } - var banASN domain.BanASN if errOpts := domain.NewBanASN(author.SteamID, targetID, duration, reason, reason.String(), modNote, domain.Bot, asNum, domain.Banned, &banASN); errOpts != nil { @@ -919,7 +850,7 @@ func (h discordService) onBanASN(ctx context.Context, _ *discordgo.Session, return nil, domain.ErrCommandFailed } - return BanASNMessage(asNum, asnRecords), nil + return BanASNMessage(asNum), nil } func (h discordService) onBanIP(ctx context.Context, _ *discordgo.Session, @@ -936,7 +867,7 @@ func (h discordService) onBanIP(ctx context.Context, _ *discordgo.Session, _, network, errParseCIDR := net.ParseCIDR(cidr) if errParseCIDR != nil { - return nil, errors.Join(errParseCIDR, domain.ErrInvalidIP) + return nil, errors.Join(errParseCIDR, domain.ErrNetworkInvalidIP) } duration, errDuration := util.ParseDuration(opts[domain.OptDuration].StringValue()) diff --git a/internal/discord/responses.go b/internal/discord/responses.go index ad850219d..11bac3655 100644 --- a/internal/discord/responses.go +++ b/internal/discord/responses.go @@ -11,7 +11,6 @@ import ( "github.com/bwmarrin/discordgo" "github.com/leighmacdonald/gbans/internal/domain" "github.com/leighmacdonald/gbans/internal/thirdparty" - "github.com/leighmacdonald/gbans/pkg/ip2location" "github.com/leighmacdonald/gbans/pkg/util" "github.com/leighmacdonald/steamid/v4/steamid" "github.com/olekukonko/tablewriter" @@ -471,9 +470,9 @@ func WarningMessage(newWarning domain.NewUserWarning, banSteam domain.BanSteam, } func CheckMessage(player domain.Person, ban domain.BannedSteamPerson, banURL string, author domain.Person, - oldBans []domain.BannedSteamPerson, bannedNets []domain.BanCIDR, asn ip2location.ASNRecord, - location ip2location.LocationRecord, - proxy ip2location.ProxyRecord, logData *thirdparty.LogsTFResult, + oldBans []domain.BannedSteamPerson, bannedNets []domain.BanCIDR, asn domain.NetworkASN, + location domain.NetworkLocation, + proxy domain.NetworkProxy, logData *thirdparty.LogsTFResult, ) *discordgo.MessageEmbed { msgEmbed := NewEmbed() @@ -597,7 +596,7 @@ func CheckMessage(player domain.Person, ban domain.BannedSteamPerson, banURL str msgEmbed.Embed().AddField("Ban/Muted", banStateStr) - if player.IPAddr != nil { + if player.IPAddr.IsValid() { msgEmbed.Embed().AddField("Last IP", player.IPAddr.String()).MakeFieldInline() } @@ -680,12 +679,11 @@ func UnbanMessage(person domain.PersonInfo) *discordgo.MessageEmbed { return msgEmbed.Embed().Truncate().MessageEmbed } -func UnbanASNMessage(asn int64, asnNetworks ip2location.ASNRecords) *discordgo.MessageEmbed { +func UnbanASNMessage(asn int64) *discordgo.MessageEmbed { return NewEmbed("ASN Networks Unbanned Successfully"). Embed(). SetColor(ColourSuccess). AddField("ASN", fmt.Sprintf("%d", asn)). - AddField("Hosts", fmt.Sprintf("%d", asnNetworks.Hosts())). Truncate().MessageEmbed } @@ -1075,11 +1073,10 @@ func MuteMessage(banSteam domain.BanSteam) *discordgo.MessageEmbed { Embed().Truncate().MessageEmbed } -func BanASNMessage(asNum int64, asnRecords ip2location.ASNRecords) *discordgo.MessageEmbed { +func BanASNMessage(asNum int64) *discordgo.MessageEmbed { return NewEmbed("ASN BanSteam Created Successfully").Embed(). SetColor(ColourSuccess). AddField("ASNum", fmt.Sprintf("%d", asNum)). - AddField("Total IPs Blocked", fmt.Sprintf("%d", asnRecords.Hosts())). Truncate(). MessageEmbed } diff --git a/internal/domain/ban.go b/internal/domain/ban.go index 8f629649c..8d5403ffa 100644 --- a/internal/domain/ban.go +++ b/internal/domain/ban.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "net/netip" "time" "github.com/leighmacdonald/steamid/v4/steamid" @@ -14,7 +15,7 @@ type BanSteamRepository interface { Save(ctx context.Context, ban *BanSteam) error GetBySteamID(ctx context.Context, sid64 steamid.SteamID, deletedOk bool) (BannedSteamPerson, error) GetByBanID(ctx context.Context, banID int64, deletedOk bool) (BannedSteamPerson, error) - GetByLastIP(ctx context.Context, lastIP net.IP, deletedOk bool) (BannedSteamPerson, error) + GetByLastIP(ctx context.Context, lastIP netip.Addr, deletedOk bool) (BannedSteamPerson, error) Delete(ctx context.Context, ban *BanSteam, hardDelete bool) error Get(ctx context.Context, filter SteamBansQueryFilter) ([]BannedSteamPerson, int64, error) ExpiredBans(ctx context.Context) ([]BanSteam, error) @@ -24,10 +25,10 @@ type BanSteamRepository interface { type BanSteamUsecase interface { IsFriendBanned(steamID steamid.SteamID) (steamid.SteamID, bool) - IsOnIPWithBan(ctx context.Context, curUser PersonInfo, steamID steamid.SteamID, address net.IP) (bool, error) + IsOnIPWithBan(ctx context.Context, curUser PersonInfo, steamID steamid.SteamID, address netip.Addr) (bool, error) GetBySteamID(ctx context.Context, sid64 steamid.SteamID, deletedOk bool) (BannedSteamPerson, error) GetByBanID(ctx context.Context, banID int64, deletedOk bool) (BannedSteamPerson, error) - GetByLastIP(ctx context.Context, lastIP net.IP, deletedOk bool) (BannedSteamPerson, error) + GetByLastIP(ctx context.Context, lastIP netip.Addr, deletedOk bool) (BannedSteamPerson, error) Save(ctx context.Context, ban *BanSteam) error Ban(ctx context.Context, curUser PersonInfo, banSteam *BanSteam) error Unban(ctx context.Context, targetSID steamid.SteamID, reason string) (bool, error) @@ -62,7 +63,7 @@ type BanGroupUsecase interface { } type BanNetRepository interface { - GetByAddress(ctx context.Context, ipAddr net.IP) ([]BanCIDR, error) + GetByAddress(ctx context.Context, ipAddr netip.Addr) ([]BanCIDR, error) GetByID(ctx context.Context, netID int64, banNet *BanCIDR) error Get(ctx context.Context, filter CIDRBansQueryFilter) ([]BannedCIDRPerson, int64, error) Save(ctx context.Context, banNet *BanCIDR) error @@ -72,7 +73,7 @@ type BanNetRepository interface { type BanNetUsecase interface { Ban(ctx context.Context, banNet *BanCIDR) error - GetByAddress(ctx context.Context, ipAddr net.IP) ([]BanCIDR, error) + GetByAddress(ctx context.Context, ipAddr netip.Addr) ([]BanCIDR, error) GetByID(ctx context.Context, netID int64, banNet *BanCIDR) error Get(ctx context.Context, filter CIDRBansQueryFilter) ([]BannedCIDRPerson, int64, error) Save(ctx context.Context, banNet *BanCIDR) error diff --git a/internal/domain/errors.go b/internal/domain/errors.go index c8ee56220..a0d31d7d9 100644 --- a/internal/domain/errors.go +++ b/internal/domain/errors.go @@ -36,6 +36,10 @@ var ( ErrInvalidPattern = errors.New("invalid pattern") ErrInvalidFilterID = errors.New("invalid fiter ID") ErrInitNetBlocks = errors.New("failed to load net blocks") + ErrNetworkInvalidIP = errors.New("invalid ip") + ErrNetworkLocationUnknown = errors.New("unknown location record") + ErrNetworkASNUnknown = errors.New("unknown asn record") + ErrNetworkProxyUnknown = errors.New("no proxy record") ErrInitNetWhitelist = errors.New("failed to load net block whitelists") ErrHTTPServer = errors.New("http listener returned unexpected error") ErrNotificationSteamIDs = errors.New("failed to load notification steam ids") @@ -60,6 +64,7 @@ var ( ErrPatreonInvalidCampaign = errors.New("patreon campaign does not exist") ErrPatreonFetchPledges = errors.New("failed to fetch patreon pledges") ErrRegisterCommand = errors.New("failed to register discord command") + ErrMissingParam = errors.New("failed to request at least one required parameter") ErrBanDoesNotExist = errors.New("ban does not exist") ErrSteamUnset = errors.New("must set steam id. see /set_steam command") ErrFetchClassStats = errors.New("failed to fetch class stats") @@ -92,12 +97,8 @@ var ( ErrUUIDCreate = errors.New("failed to generate new uuid") ErrReportExists = errors.New("cannot create report while existing report open") ErrAssetCreateFailed = errors.New("failed to create asset") - ErrAssetPut = errors.New("unable to create asset on remote store") ErrAssetGet = errors.New("failed to get asset from client") - ErrAssetSave = errors.New("failed to save asset metadata") ErrInvalidFormat = errors.New("invalid format") - ErrDuplicateMediaName = errors.New("duplicate media name") - ErrSaveMedia = errors.New("could not save media") ErrFetchMedia = errors.New("failed to fetch media asset") ErrEmptyToken = errors.New("invalid Access token decoded") ErrContestLoadEntries = errors.New("failed to load existing contest entries") diff --git a/internal/domain/mocks/MockNetworkRepository.go b/internal/domain/mocks/MockNetworkRepository.go index 3f3908c62..04e9fac37 100644 --- a/internal/domain/mocks/MockNetworkRepository.go +++ b/internal/domain/mocks/MockNetworkRepository.go @@ -494,7 +494,7 @@ func (_c *MockNetworkRepository_InsertBlockListData_Call) RunAndReturn(run func( } // QueryConnectionHistory provides a mock function with given fields: ctx, opts -func (_m *MockNetworkRepository) QueryConnectionHistory(ctx context.Context, opts domain.ConnectionHistoryQueryFilter) ([]domain.PersonConnection, int64, error) { +func (_m *MockNetworkRepository) QueryConnectionHistory(ctx context.Context, opts domain.ConnectionHistoryQuery) ([]domain.PersonConnection, int64, error) { ret := _m.Called(ctx, opts) if len(ret) == 0 { @@ -504,10 +504,10 @@ func (_m *MockNetworkRepository) QueryConnectionHistory(ctx context.Context, opt var r0 []domain.PersonConnection var r1 int64 var r2 error - if rf, ok := ret.Get(0).(func(context.Context, domain.ConnectionHistoryQueryFilter) ([]domain.PersonConnection, int64, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, domain.ConnectionHistoryQuery) ([]domain.PersonConnection, int64, error)); ok { return rf(ctx, opts) } - if rf, ok := ret.Get(0).(func(context.Context, domain.ConnectionHistoryQueryFilter) []domain.PersonConnection); ok { + if rf, ok := ret.Get(0).(func(context.Context, domain.ConnectionHistoryQuery) []domain.PersonConnection); ok { r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { @@ -515,13 +515,13 @@ func (_m *MockNetworkRepository) QueryConnectionHistory(ctx context.Context, opt } } - if rf, ok := ret.Get(1).(func(context.Context, domain.ConnectionHistoryQueryFilter) int64); ok { + if rf, ok := ret.Get(1).(func(context.Context, domain.ConnectionHistoryQuery) int64); ok { r1 = rf(ctx, opts) } else { r1 = ret.Get(1).(int64) } - if rf, ok := ret.Get(2).(func(context.Context, domain.ConnectionHistoryQueryFilter) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, domain.ConnectionHistoryQuery) error); ok { r2 = rf(ctx, opts) } else { r2 = ret.Error(2) @@ -537,14 +537,14 @@ type MockNetworkRepository_QueryConnectionHistory_Call struct { // QueryConnectionHistory is a helper method to define mock.On call // - ctx context.Context -// - opts domain.ConnectionHistoryQueryFilter +// - opts domain.ConnectionHistoryQuery func (_e *MockNetworkRepository_Expecter) QueryConnectionHistory(ctx interface{}, opts interface{}) *MockNetworkRepository_QueryConnectionHistory_Call { return &MockNetworkRepository_QueryConnectionHistory_Call{Call: _e.mock.On("QueryConnectionHistory", ctx, opts)} } -func (_c *MockNetworkRepository_QueryConnectionHistory_Call) Run(run func(ctx context.Context, opts domain.ConnectionHistoryQueryFilter)) *MockNetworkRepository_QueryConnectionHistory_Call { +func (_c *MockNetworkRepository_QueryConnectionHistory_Call) Run(run func(ctx context.Context, opts domain.ConnectionHistoryQuery)) *MockNetworkRepository_QueryConnectionHistory_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(domain.ConnectionHistoryQueryFilter)) + run(args[0].(context.Context), args[1].(domain.ConnectionHistoryQuery)) }) return _c } @@ -554,7 +554,7 @@ func (_c *MockNetworkRepository_QueryConnectionHistory_Call) Return(_a0 []domain return _c } -func (_c *MockNetworkRepository_QueryConnectionHistory_Call) RunAndReturn(run func(context.Context, domain.ConnectionHistoryQueryFilter) ([]domain.PersonConnection, int64, error)) *MockNetworkRepository_QueryConnectionHistory_Call { +func (_c *MockNetworkRepository_QueryConnectionHistory_Call) RunAndReturn(run func(context.Context, domain.ConnectionHistoryQuery) ([]domain.PersonConnection, int64, error)) *MockNetworkRepository_QueryConnectionHistory_Call { _c.Call.Return(run) return _c } diff --git a/internal/domain/mocks/MockNetworkUsecase.go b/internal/domain/mocks/MockNetworkUsecase.go index 54ee3de6f..9ad91585c 100644 --- a/internal/domain/mocks/MockNetworkUsecase.go +++ b/internal/domain/mocks/MockNetworkUsecase.go @@ -629,7 +629,7 @@ func (_c *MockNetworkUsecase_LoadNetBlocks_Call) RunAndReturn(run func(context.C } // QueryConnectionHistory provides a mock function with given fields: ctx, opts -func (_m *MockNetworkUsecase) QueryConnectionHistory(ctx context.Context, opts domain.ConnectionHistoryQueryFilter) ([]domain.PersonConnection, int64, error) { +func (_m *MockNetworkUsecase) QueryConnectionHistory(ctx context.Context, opts domain.ConnectionHistoryQuery) ([]domain.PersonConnection, int64, error) { ret := _m.Called(ctx, opts) if len(ret) == 0 { @@ -639,10 +639,10 @@ func (_m *MockNetworkUsecase) QueryConnectionHistory(ctx context.Context, opts d var r0 []domain.PersonConnection var r1 int64 var r2 error - if rf, ok := ret.Get(0).(func(context.Context, domain.ConnectionHistoryQueryFilter) ([]domain.PersonConnection, int64, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, domain.ConnectionHistoryQuery) ([]domain.PersonConnection, int64, error)); ok { return rf(ctx, opts) } - if rf, ok := ret.Get(0).(func(context.Context, domain.ConnectionHistoryQueryFilter) []domain.PersonConnection); ok { + if rf, ok := ret.Get(0).(func(context.Context, domain.ConnectionHistoryQuery) []domain.PersonConnection); ok { r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { @@ -650,13 +650,13 @@ func (_m *MockNetworkUsecase) QueryConnectionHistory(ctx context.Context, opts d } } - if rf, ok := ret.Get(1).(func(context.Context, domain.ConnectionHistoryQueryFilter) int64); ok { + if rf, ok := ret.Get(1).(func(context.Context, domain.ConnectionHistoryQuery) int64); ok { r1 = rf(ctx, opts) } else { r1 = ret.Get(1).(int64) } - if rf, ok := ret.Get(2).(func(context.Context, domain.ConnectionHistoryQueryFilter) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, domain.ConnectionHistoryQuery) error); ok { r2 = rf(ctx, opts) } else { r2 = ret.Error(2) @@ -672,14 +672,14 @@ type MockNetworkUsecase_QueryConnectionHistory_Call struct { // QueryConnectionHistory is a helper method to define mock.On call // - ctx context.Context -// - opts domain.ConnectionHistoryQueryFilter +// - opts domain.ConnectionHistoryQuery func (_e *MockNetworkUsecase_Expecter) QueryConnectionHistory(ctx interface{}, opts interface{}) *MockNetworkUsecase_QueryConnectionHistory_Call { return &MockNetworkUsecase_QueryConnectionHistory_Call{Call: _e.mock.On("QueryConnectionHistory", ctx, opts)} } -func (_c *MockNetworkUsecase_QueryConnectionHistory_Call) Run(run func(ctx context.Context, opts domain.ConnectionHistoryQueryFilter)) *MockNetworkUsecase_QueryConnectionHistory_Call { +func (_c *MockNetworkUsecase_QueryConnectionHistory_Call) Run(run func(ctx context.Context, opts domain.ConnectionHistoryQuery)) *MockNetworkUsecase_QueryConnectionHistory_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(domain.ConnectionHistoryQueryFilter)) + run(args[0].(context.Context), args[1].(domain.ConnectionHistoryQuery)) }) return _c } @@ -689,7 +689,7 @@ func (_c *MockNetworkUsecase_QueryConnectionHistory_Call) Return(_a0 []domain.Pe return _c } -func (_c *MockNetworkUsecase_QueryConnectionHistory_Call) RunAndReturn(run func(context.Context, domain.ConnectionHistoryQueryFilter) ([]domain.PersonConnection, int64, error)) *MockNetworkUsecase_QueryConnectionHistory_Call { +func (_c *MockNetworkUsecase_QueryConnectionHistory_Call) RunAndReturn(run func(context.Context, domain.ConnectionHistoryQuery) ([]domain.PersonConnection, int64, error)) *MockNetworkUsecase_QueryConnectionHistory_Call { _c.Call.Return(run) return _c } diff --git a/internal/domain/net.go b/internal/domain/net.go index f38afd4d7..4260b8ca2 100644 --- a/internal/domain/net.go +++ b/internal/domain/net.go @@ -3,6 +3,8 @@ package domain import ( "context" "net" + "net/netip" + "time" "github.com/leighmacdonald/gbans/pkg/ip2location" "github.com/leighmacdonald/steamid/v4/steamid" @@ -10,34 +12,31 @@ import ( type NetworkUsecase interface { LoadNetBlocks(ctx context.Context) error - GetASNRecordByIP(ctx context.Context, ipAddr net.IP, asnRecord *ip2location.ASNRecord) error - GetASNRecordsByNum(ctx context.Context, asNum int64) (ip2location.ASNRecords, error) - GetLocationRecord(ctx context.Context, ipAddr net.IP, record *ip2location.LocationRecord) error - GetProxyRecord(ctx context.Context, ipAddr net.IP, proxyRecord *ip2location.ProxyRecord) error + GetASNRecordsByNum(ctx context.Context, asNum int64) ([]NetworkASN, error) InsertBlockListData(ctx context.Context, blockListData *ip2location.BlockListData) error GetPersonIPHistory(ctx context.Context, sid64 steamid.SteamID, limit uint64) (PersonConnections, error) GetPlayerMostRecentIP(ctx context.Context, steamID steamid.SteamID) net.IP - QueryConnectionHistory(ctx context.Context, opts ConnectionHistoryQueryFilter) ([]PersonConnection, int64, error) - QueryConnectionBySteamID(ctx context.Context, opts ConnectionHistoryBySteamIDQueryFilter) ([]PersonConnection, int64, error) + QueryConnectionHistory(ctx context.Context, opts ConnectionHistoryQuery) ([]PersonConnection, int64, error) AddConnectionHistory(ctx context.Context, conn *PersonConnection) error - IsMatch(addr net.IP) (string, bool) + IsMatch(addr netip.Addr) (string, bool) AddWhitelist(id int, network *net.IPNet) RemoveWhitelist(id int) AddRemoteSource(ctx context.Context, name string, url string) (int64, error) + QueryNetwork(ctx context.Context, ip netip.Addr) (NetworkDetails, error) } + type NetworkRepository interface { - QueryConnectionHistory(ctx context.Context, opts ConnectionHistoryQueryFilter) ([]PersonConnection, int64, error) - QueryConnectionBySteamID(ctx context.Context, opts ConnectionHistoryBySteamIDQueryFilter) ([]PersonConnection, int64, error) + QueryConnections(ctx context.Context, opts ConnectionHistoryQuery) ([]PersonConnection, int64, error) GetPersonIPHistory(ctx context.Context, sid64 steamid.SteamID, limit uint64) (PersonConnections, error) AddConnectionHistory(ctx context.Context, conn *PersonConnection) error GetPlayerMostRecentIP(ctx context.Context, steamID steamid.SteamID) net.IP - GetASNRecordsByNum(ctx context.Context, asNum int64) (ip2location.ASNRecords, error) - GetASNRecordByIP(ctx context.Context, ipAddr net.IP, asnRecord *ip2location.ASNRecord) error - GetLocationRecord(ctx context.Context, ipAddr net.IP, record *ip2location.LocationRecord) error - GetProxyRecord(ctx context.Context, ipAddr net.IP, proxyRecord *ip2location.ProxyRecord) error + GetASNRecordsByNum(ctx context.Context, asNum int64) ([]NetworkASN, error) + GetASNRecordByIP(ctx context.Context, ipAddr netip.Addr) (NetworkASN, error) + GetLocationRecord(ctx context.Context, ipAddr netip.Addr) (NetworkLocation, error) + GetProxyRecord(ctx context.Context, ipAddr netip.Addr) (NetworkProxy, error) InsertBlockListData(ctx context.Context, blockListData *ip2location.BlockListData) error - GetSteamIDsAtIP(ctx context.Context, ipNet *net.IPNet) (steamid.Collection, error) } + type CIDRBlockSource struct { CIDRBlockSourceID int `json:"cidr_block_source_id"` Name string `json:"name"` @@ -51,3 +50,45 @@ type CIDRBlockWhitelist struct { Address *net.IPNet `json:"address"` TimeStamped } + +type NetworkDetailsQuery struct { + QueryFilter + IP netip.Addr `json:"ip"` +} + +type NetworkDetails struct { + Location NetworkLocation `json:"location"` + Asn NetworkASN `json:"asn"` + Proxy NetworkProxy `json:"proxy"` +} + +type NetworkLocation struct { + CIDR string `json:"cidr"` + CountryCode string `json:"country_code"` + CountryName string `json:"country_name"` + RegionName string `json:"region_name"` + CityName string `json:"city_name"` + LatLong ip2location.LatLong `json:"lat_long"` +} + +type NetworkASN struct { + CIDR string `json:"cidr"` + ASNum uint64 `json:"as_num"` + ASName string `json:"as_name"` +} + +type NetworkProxy struct { + CIDR string `json:"cidr"` + ProxyType ip2location.ProxyType `json:"proxy_type"` + CountryCode string `json:"country_code"` + CountryName string `json:"country_name"` + RegionName string `json:"region_name"` + CityName string `json:"city_name"` + ISP string `json:"isp"` + Domain string `json:"domain"` + UsageType ip2location.UsageType `json:"usage_type"` + ASN int64 `json:"as_num"` //nolint:tagliatelle + AS string `json:"as_name"` //nolint:tagliatelle + LastSeen time.Time `json:"last_seen"` + Threat ip2location.ThreatType `json:"threat"` +} diff --git a/internal/domain/person.go b/internal/domain/person.go index 5302919dd..0ddd5c04e 100644 --- a/internal/domain/person.go +++ b/internal/domain/person.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "net/netip" "time" "github.com/gofrs/uuid/v5" @@ -123,7 +124,7 @@ type Person struct { Muted bool `json:"muted"` IsNew bool `json:"-"` DiscordID string `json:"discord_id"` - IPAddr net.IP `json:"-"` // TODO Allow json for admins endpoints + IPAddr netip.Addr `json:"-"` // TODO Allow json for admins endpoints CommunityBanned bool `json:"community_banned"` VACBans int `json:"vac_bans"` GameBans int `json:"game_bans"` @@ -184,7 +185,6 @@ func NewPerson(sid64 steamid.SteamID) Person { Muted: false, IsNew: true, DiscordID: "", - IPAddr: nil, CommunityBanned: false, VACBans: 0, GameBans: 0, @@ -265,7 +265,7 @@ func NewPersonAuth(sid64 steamid.SteamID, addr net.IP, fingerPrint string) Perso type PersonConnection struct { PersonConnectionID int64 `json:"person_connection_id"` - IPAddr net.IP `json:"ip_addr"` + IPAddr netip.Addr `json:"ip_addr"` SteamID steamid.SteamID `json:"steam_id"` PersonaName string `json:"persona_name"` ServerID int `json:"server_id"` diff --git a/internal/domain/query_filter.go b/internal/domain/query_filter.go index 5e64f6071..1f272a6a9 100644 --- a/internal/domain/query_filter.go +++ b/internal/domain/query_filter.go @@ -107,15 +107,13 @@ func (f ChatHistoryQueryFilter) SourceSteamID() (steamid.SteamID, bool) { return sid, sid.Valid() } -type ConnectionHistoryQueryFilter struct { - QueryFilter - IP string `json:"ip"` - SourceIDField -} - -type ConnectionHistoryBySteamIDQueryFilter struct { +type ConnectionHistoryQuery struct { QueryFilter SourceIDField + CIDR string `json:"cidr"` + ASN int `json:"asn"` + Sid64 int64 + Network string } type PlayerQuery struct { diff --git a/internal/network/network.go b/internal/network/network.go index f18d0b8b8..e58f58f11 100644 --- a/internal/network/network.go +++ b/internal/network/network.go @@ -7,6 +7,7 @@ import ( "io" "net" "net/http" + "net/netip" "regexp" "strings" "sync" @@ -35,19 +36,21 @@ func NewBlocker() *Blocker { } } -func (b *Blocker) IsMatch(addr net.IP) (string, bool) { +func (b *Blocker) IsMatch(addr netip.Addr) (string, bool) { b.RLock() defer b.RUnlock() + address := net.ParseIP(addr.String()) + for _, whitelisted := range b.whitelisted { - if whitelisted.Contains(addr) { + if whitelisted.Contains(address) { return "", false } } for name, networks := range b.blocks { for _, block := range networks { - if block.Contains(addr) { + if block.Contains(address) { return name, true } } diff --git a/internal/network/network_repository.go b/internal/network/network_repository.go index f740b23de..6c7af67d6 100644 --- a/internal/network/network_repository.go +++ b/internal/network/network_repository.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "net" + "net/netip" "time" sq "github.com/Masterminds/squirrel" @@ -24,13 +25,16 @@ func NewNetworkRepository(db database.Database) domain.NetworkRepository { return networkRepository{db: db} } -func (r networkRepository) QueryConnectionBySteamID(ctx context.Context, opts domain.ConnectionHistoryBySteamIDQueryFilter) ([]domain.PersonConnection, int64, error) { - sid, ok := opts.SourceSteamID(ctx) - if !ok { - return nil, 0, domain.ErrInvalidSID +func (r networkRepository) QueryConnections(ctx context.Context, opts domain.ConnectionHistoryQuery) ([]domain.PersonConnection, int64, error) { + var constraints sq.And + + if opts.Sid64 > 0 { + constraints = append(constraints, sq.Eq{"steam_id": opts.Sid64}) } - constraints := sq.And{sq.Eq{"c.steam_id": sid}} + if opts.Network != "" { + constraints = append(constraints, sq.Expr("ip_addr <<= ?::ip4r", opts.Network)) + } selectBuilder := r.db.Builder(). Select("distinct on (c.ip_addr) c.ip_addr", "c.person_connection_id", "c.persona_name", @@ -50,7 +54,7 @@ func (r networkRepository) QueryConnectionBySteamID(ctx context.Context, opts do var messages []domain.PersonConnection - rows, errQuery := r.db.QueryBuilder(ctx, builder) + rows, errQuery := r.db.QueryBuilder(ctx, builder.Where(constraints)) if errQuery != nil { return nil, 0, r.db.DBErr(errQuery) } @@ -105,82 +109,6 @@ func (r networkRepository) QueryConnectionBySteamID(ctx context.Context, opts do return messages, count, nil } -func (r networkRepository) QueryConnectionHistory(ctx context.Context, opts domain.ConnectionHistoryQueryFilter) ([]domain.PersonConnection, int64, error) { - builder := r.db. - Builder(). - Select("c.person_connection_id", "c.steam_id", - "c.ip_addr", "c.persona_name", "c.created_on", "c.server_id", "r.short_name", "r.name"). - From("person_connections c"). - LeftJoin("server r USING(server_id)"). - GroupBy("c.person_connection_id, c.ip_addr, r.short_name", "r.name") - - var constraints sq.And - - if sid, ok := opts.SourceSteamID(ctx); ok { - constraints = append(constraints, sq.Eq{"c.steam_id": sid}) - } - - builder = opts.ApplySafeOrder(opts.ApplyLimitOffsetDefault(builder), map[string][]string{ - "c.": {"person_connection_id", "steam_id", "ip_addr", "persona_name", "created_on"}, - "r.": {"short_name", "name"}, - }, "person_connection_id") - - var messages []domain.PersonConnection - - rows, errQuery := r.db.QueryBuilder(ctx, builder.Where(constraints)) - if errQuery != nil { - return nil, 0, r.db.DBErr(errQuery) - } - - defer rows.Close() - - for rows.Next() { - var ( - connHistory domain.PersonConnection - steamID int64 - serverID *int - shortName *string - name *string - ) - - if errScan := rows.Scan(&connHistory.PersonConnectionID, - &steamID, - &connHistory.IPAddr, - &connHistory.PersonaName, - &connHistory.CreatedOn, - &serverID, &shortName, &name); errScan != nil { - return nil, 0, r.db.DBErr(errScan) - } - - // Added later in dev, so can be legacy data w/o a server_id - if serverID != nil && shortName != nil && name != nil { - connHistory.ServerID = *serverID - connHistory.ServerNameShort = *shortName - connHistory.ServerName = *name - } - - connHistory.SteamID = steamid.New(steamID) - - messages = append(messages, connHistory) - } - - if messages == nil { - return []domain.PersonConnection{}, 0, nil - } - - count, errCount := r.db.GetCount(ctx, r.db. - Builder(). - Select("count(c.person_connection_id)"). - From("person_connections c"). - Where(constraints)) - - if errCount != nil { - return nil, 0, r.db.DBErr(errCount) - } - - return messages, count, nil -} - func (r networkRepository) GetPersonIPHistory(ctx context.Context, sid64 steamid.SteamID, limit uint64) (domain.PersonConnections, error) { builder := r.db. Builder(). @@ -265,10 +193,10 @@ func (r networkRepository) GetPlayerMostRecentIP(ctx context.Context, steamID st return addr } -func (r networkRepository) GetASNRecordsByNum(ctx context.Context, asNum int64) (ip2location.ASNRecords, error) { +func (r networkRepository) GetASNRecordsByNum(ctx context.Context, asNum int64) ([]domain.NetworkASN, error) { query := r.db. Builder(). - Select("ip_from", "ip_to", "cidr", "as_num", "as_name"). + Select("cidr::text", "as_num", "as_name"). From("net_asn"). Where(sq.Eq{"as_num": asNum}) @@ -279,12 +207,12 @@ func (r networkRepository) GetASNRecordsByNum(ctx context.Context, asNum int64) defer rows.Close() - records := ip2location.ASNRecords{} + var records []domain.NetworkASN for rows.Next() { - var asnRecord ip2location.ASNRecord + var asnRecord domain.NetworkASN if errScan := rows. - Scan(&asnRecord.IPFrom, &asnRecord.IPTo, &asnRecord.CIDR, &asnRecord.ASNum, &asnRecord.ASName); errScan != nil { + Scan(&asnRecord.CIDR, &asnRecord.ASNum, &asnRecord.ASName); errScan != nil { return nil, r.db.DBErr(errScan) } @@ -294,51 +222,57 @@ func (r networkRepository) GetASNRecordsByNum(ctx context.Context, asNum int64) return records, nil } -func (r networkRepository) GetASNRecordByIP(ctx context.Context, ipAddr net.IP, asnRecord *ip2location.ASNRecord) error { +func (r networkRepository) GetASNRecordByIP(ctx context.Context, ipAddr netip.Addr) (domain.NetworkASN, error) { const query = ` - SELECT ip_from, ip_to, cidr, as_num, as_name + SELECT ip_range::text, as_num, as_name FROM net_asn - WHERE $1::inet <@ ip_range + WHERE ip_range >>= $1 LIMIT 1` + var asnRecord domain.NetworkASN + if errQuery := r.db. QueryRow(ctx, query, ipAddr.String()). - Scan(&asnRecord.IPFrom, &asnRecord.IPTo, &asnRecord.CIDR, &asnRecord.ASNum, &asnRecord.ASName); errQuery != nil { - return r.db.DBErr(errQuery) + Scan(&asnRecord.CIDR, &asnRecord.ASNum, &asnRecord.ASName); errQuery != nil { + return asnRecord, r.db.DBErr(errQuery) } - return nil + return asnRecord, nil } -func (r networkRepository) GetLocationRecord(ctx context.Context, ipAddr net.IP, record *ip2location.LocationRecord) error { +func (r networkRepository) GetLocationRecord(ctx context.Context, ipAddr netip.Addr) (domain.NetworkLocation, error) { const query = ` - SELECT ip_from, ip_to, country_code, country_name, region_name, city_name, ST_Y(location), ST_X(location) + SELECT ip_range::text, country_code, country_name, region_name, city_name, ST_Y(location), ST_X(location) FROM net_location - WHERE ip_range @> $1::inet` + WHERE ip_range >>= $1` + + var record domain.NetworkLocation if errQuery := r.db.QueryRow(ctx, query, ipAddr.String()). - Scan(&record.IPFrom, &record.IPTo, &record.CountryCode, &record.CountryName, &record.RegionName, + Scan(&record.CIDR, &record.CountryCode, &record.CountryName, &record.RegionName, &record.CityName, &record.LatLong.Latitude, &record.LatLong.Longitude); errQuery != nil { - return r.db.DBErr(errQuery) + return record, r.db.DBErr(errQuery) } - return nil + return record, nil } -func (r networkRepository) GetProxyRecord(ctx context.Context, ipAddr net.IP, proxyRecord *ip2location.ProxyRecord) error { +func (r networkRepository) GetProxyRecord(ctx context.Context, ipAddr netip.Addr) (domain.NetworkProxy, error) { const query = ` - SELECT ip_from, ip_to, proxy_type, country_code, country_name, region_name, + SELECT ip_range::text, proxy_type, country_code, country_name, region_name, city_name, isp, domain_used, usage_type, as_num, as_name, last_seen, threat FROM net_proxy - WHERE $1::inet <@ ip_range` + WHERE ip_range >>= $1` + + var proxyRecord domain.NetworkProxy if errQuery := r.db.QueryRow(ctx, query, ipAddr.String()). - Scan(&proxyRecord.IPFrom, &proxyRecord.IPTo, &proxyRecord.ProxyType, &proxyRecord.CountryCode, &proxyRecord.CountryName, &proxyRecord.RegionName, &proxyRecord.CityName, &proxyRecord.ISP, + Scan(&proxyRecord.CIDR, &proxyRecord.ProxyType, &proxyRecord.CountryCode, &proxyRecord.CountryName, &proxyRecord.RegionName, &proxyRecord.CityName, &proxyRecord.ISP, &proxyRecord.Domain, &proxyRecord.UsageType, &proxyRecord.ASN, &proxyRecord.AS, &proxyRecord.LastSeen, &proxyRecord.Threat); errQuery != nil { - return r.db.DBErr(errQuery) + return proxyRecord, r.db.DBErr(errQuery) } - return nil + return proxyRecord, nil } func (r networkRepository) loadASN(ctx context.Context, records []ip2location.ASNRecord) error { @@ -349,13 +283,13 @@ func (r networkRepository) loadASN(ctx context.Context, records []ip2location.AS } const query = ` - INSERT INTO net_asn (ip_from, ip_to, cidr, as_num, as_name, ip_range) - VALUES($1, $2, $3, $4, $5, iprange($1, $2))` + INSERT INTO net_asn (ip_range, cidr, as_num, as_name) + VALUES($1, $2, $3, $4)` batch := pgx.Batch{} for recordIdx, asnRecord := range records { - batch.Queue(query, asnRecord.IPFrom, asnRecord.IPTo, asnRecord.CIDR, asnRecord.ASNum, asnRecord.ASName) + batch.Queue(query, fmt.Sprintf("%s-%s", asnRecord.IPFrom, asnRecord.IPTo), asnRecord.CIDR, asnRecord.ASNum, asnRecord.ASName) if recordIdx > 0 && recordIdx%100000 == 0 || len(records) == recordIdx+1 { if batch.Len() > 0 { @@ -392,13 +326,13 @@ func (r networkRepository) loadLocation(ctx context.Context, records []ip2locati } const query = ` - INSERT INTO net_location (ip_from, ip_to, country_code, country_name, region_name, city_name, location, ip_range) - VALUES($1, $2, $3, $4, $5, $6, ST_SetSRID(ST_MakePoint($8, $7), 4326), iprange($1, $2))` + INSERT INTO net_location (ip_range, country_code, country_name, region_name, city_name, location) + VALUES($1::ip4r, $2, $3, $4, $5, ST_SetSRID(ST_MakePoint($7, $6), 4326) )` batch := pgx.Batch{} for recordIdx, locationRecord := range records { - batch.Queue(query, locationRecord.IPFrom, locationRecord.IPTo, locationRecord.CountryCode, locationRecord.CountryName, locationRecord.RegionName, locationRecord.CityName, locationRecord.LatLong.Latitude, locationRecord.LatLong.Longitude) + batch.Queue(query, fmt.Sprintf("%s-%s", locationRecord.IPFrom, locationRecord.IPTo), locationRecord.CountryCode, locationRecord.CountryName, locationRecord.RegionName, locationRecord.CityName, locationRecord.LatLong.Latitude, locationRecord.LatLong.Longitude) if recordIdx > 0 && recordIdx%100000 == 0 || len(records) == recordIdx+1 { if batch.Len() > 0 { @@ -435,14 +369,14 @@ func (r networkRepository) loadProxies(ctx context.Context, records []ip2locatio } const query = ` - INSERT INTO net_proxy (ip_from, ip_to, proxy_type, country_code, country_name, region_name, city_name, isp, - domain_used, usage_type, as_num, as_name, last_seen, threat, ip_range) - VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, iprange($1, $2))` + INSERT INTO net_proxy (ip_range, proxy_type, country_code, country_name, region_name, city_name, isp, + domain_used, usage_type, as_num, as_name, last_seen, threat) + VALUES(ip4r($1::ip4, $2::ip4), $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)` batch := pgx.Batch{} for recordIdx, proxyRecord := range records { - batch.Queue(query, proxyRecord.IPFrom, proxyRecord.IPTo, proxyRecord.ProxyType, proxyRecord.CountryCode, proxyRecord.CountryName, proxyRecord.RegionName, proxyRecord.CityName, + batch.Queue(query, proxyRecord.IPFrom.To4().String(), proxyRecord.IPTo.To4().String(), proxyRecord.ProxyType, proxyRecord.CountryCode, proxyRecord.CountryName, proxyRecord.RegionName, proxyRecord.CityName, proxyRecord.ISP, proxyRecord.Domain, proxyRecord.UsageType, proxyRecord.ASN, proxyRecord.AS, proxyRecord.LastSeen, proxyRecord.Threat) if recordIdx > 0 && recordIdx%100000 == 0 || len(records) == recordIdx+1 { @@ -497,34 +431,3 @@ func (r networkRepository) InsertBlockListData(ctx context.Context, blockListDat return nil } - -func (r networkRepository) GetSteamIDsAtIP(ctx context.Context, ipNet *net.IPNet) (steamid.Collection, error) { - const query = ` - SELECT DISTINCT c.steam_id - FROM person_connections c - WHERE ip_addr::inet <<= inet '%s';` - - if ipNet == nil { - return nil, domain.ErrInvalidCIDR - } - - rows, errQuery := r.db.Query(ctx, fmt.Sprintf(query, ipNet.String())) - if errQuery != nil { - return nil, r.db.DBErr(errQuery) - } - - defer rows.Close() - - var ids steamid.Collection - - for rows.Next() { - var sid64 int64 - if errScan := rows.Scan(&sid64); errScan != nil { - return nil, r.db.DBErr(errScan) - } - - ids = append(ids, steamid.New(sid64)) - } - - return ids, nil -} diff --git a/internal/network/network_service.go b/internal/network/network_service.go index 409b782b5..ad4c56709 100644 --- a/internal/network/network_service.go +++ b/internal/network/network_service.go @@ -21,38 +21,38 @@ func NewNetworkHandler(engine *gin.Engine, nu domain.NetworkUsecase, ath domain. modGrp := engine.Group("/") { mod := modGrp.Use(ath.AuthMiddleware(domain.PModerator)) - mod.POST("/api/connections", handler.onAPIQueryPersonConnections()) - mod.POST("/api/connections/steam", handler.onAPIQuerySteamIDConnections()) + mod.POST("/api/connections", handler.onAPIQueryConnections()) + mod.POST("/api/network", handler.onAPIQueryNetwork()) } } -func (h networkHandler) onAPIQueryPersonConnections() gin.HandlerFunc { +func (h networkHandler) onAPIQueryNetwork() gin.HandlerFunc { return func(ctx *gin.Context) { - var req domain.ConnectionHistoryQueryFilter + var req domain.NetworkDetailsQuery if !httphelper.Bind(ctx, &req) { return } - ipHist, totalCount, errIPHist := h.nu.QueryConnectionHistory(ctx, req) - if errIPHist != nil && !errors.Is(errIPHist, domain.ErrNoResult) { - slog.Error("Failed to query connection history", log.ErrAttr(errIPHist)) + details, err := h.nu.QueryNetwork(ctx, req.IP) + if err != nil { + slog.Error("Failed to query connection history", log.ErrAttr(err)) httphelper.ResponseErr(ctx, http.StatusInternalServerError, domain.ErrInternal) return } - ctx.JSON(http.StatusOK, domain.NewLazyResult(totalCount, ipHist)) + ctx.JSON(http.StatusOK, details) } } -func (h networkHandler) onAPIQuerySteamIDConnections() gin.HandlerFunc { +func (h networkHandler) onAPIQueryConnections() gin.HandlerFunc { return func(ctx *gin.Context) { - var req domain.ConnectionHistoryBySteamIDQueryFilter + var req domain.ConnectionHistoryQuery if !httphelper.Bind(ctx, &req) { return } - ipHist, totalCount, errIPHist := h.nu.QueryConnectionBySteamID(ctx, req) + ipHist, totalCount, errIPHist := h.nu.QueryConnectionHistory(ctx, req) if errIPHist != nil && !errors.Is(errIPHist, domain.ErrNoResult) { slog.Error("Failed to query connection history", log.ErrAttr(errIPHist)) httphelper.ResponseErr(ctx, http.StatusInternalServerError, domain.ErrInternal) diff --git a/internal/network/network_test.go b/internal/network/network_test.go index 22d4af4f9..7dbd3b8bb 100644 --- a/internal/network/network_test.go +++ b/internal/network/network_test.go @@ -2,7 +2,7 @@ package network_test import ( "context" - "net" + "net/netip" "testing" "github.com/leighmacdonald/gbans/internal/network" @@ -18,16 +18,16 @@ func TestNetworkBlocker(t *testing.T) { require.NoError(t, errAdd) require.True(t, count > 100) - name, matched := blocker.IsMatch(net.ParseIP("3.2.2.2")) + name, matched := blocker.IsMatch(netip.MustParseAddr("3.2.2.2")) require.True(t, matched) require.Equal(t, testName, name) - noName, noMatch := blocker.IsMatch(net.ParseIP("1.1.1.1")) + noName, noMatch := blocker.IsMatch(netip.MustParseAddr("1.1.1.1")) require.False(t, noMatch) require.Equal(t, "", noName) blocker.RemoveSource(testName) - _, noMatch2 := blocker.IsMatch(net.ParseIP("3.2.2.2")) + _, noMatch2 := blocker.IsMatch(netip.MustParseAddr("3.2.2.2")) require.False(t, noMatch2) } diff --git a/internal/network/network_usecase.go b/internal/network/network_usecase.go index 0d17f10ba..5e9654ea9 100644 --- a/internal/network/network_usecase.go +++ b/internal/network/network_usecase.go @@ -5,6 +5,7 @@ import ( "errors" "log/slog" "net" + "net/netip" "strings" "sync" "sync/atomic" @@ -62,8 +63,8 @@ func (u networkUsecase) Start(ctx context.Context) { continue } - parsedAddr := net.ParseIP(newServerEvent.Address) - if parsedAddr == nil { + parsedAddr, errParsedAddr := netip.ParseAddr(newServerEvent.Address) + if errParsedAddr != nil { slog.Warn("Received invalid address", slog.String("addr", newServerEvent.Address)) continue @@ -147,7 +148,7 @@ func (u networkUsecase) AddConnectionHistory(ctx context.Context, conn *domain.P return u.nr.AddConnectionHistory(ctx, conn) } -func (u networkUsecase) IsMatch(addr net.IP) (string, bool) { +func (u networkUsecase) IsMatch(addr netip.Addr) (string, bool) { return u.blocker.IsMatch(addr) } @@ -163,22 +164,10 @@ func (u networkUsecase) AddRemoteSource(ctx context.Context, name string, url st return u.blocker.AddRemoteSource(ctx, name, url) } -func (u networkUsecase) GetASNRecordByIP(ctx context.Context, ipAddr net.IP, asnRecord *ip2location.ASNRecord) error { - return u.nr.GetASNRecordByIP(ctx, ipAddr, asnRecord) -} - -func (u networkUsecase) GetASNRecordsByNum(ctx context.Context, asNum int64) (ip2location.ASNRecords, error) { +func (u networkUsecase) GetASNRecordsByNum(ctx context.Context, asNum int64) ([]domain.NetworkASN, error) { return u.nr.GetASNRecordsByNum(ctx, asNum) } -func (u networkUsecase) GetLocationRecord(ctx context.Context, ipAddr net.IP, record *ip2location.LocationRecord) error { - return u.nr.GetLocationRecord(ctx, ipAddr, record) -} - -func (u networkUsecase) GetProxyRecord(ctx context.Context, ipAddr net.IP, proxyRecord *ip2location.ProxyRecord) error { - return u.nr.GetProxyRecord(ctx, ipAddr, proxyRecord) -} - func (u networkUsecase) InsertBlockListData(ctx context.Context, blockListData *ip2location.BlockListData) error { return u.nr.InsertBlockListData(ctx, blockListData) } @@ -191,10 +180,60 @@ func (u networkUsecase) GetPlayerMostRecentIP(ctx context.Context, steamID steam return u.nr.GetPlayerMostRecentIP(ctx, steamID) } -func (u networkUsecase) QueryConnectionHistory(ctx context.Context, opts domain.ConnectionHistoryQueryFilter) ([]domain.PersonConnection, int64, error) { - return u.nr.QueryConnectionHistory(ctx, opts) +func (u networkUsecase) QueryConnectionHistory(ctx context.Context, opts domain.ConnectionHistoryQuery) ([]domain.PersonConnection, int64, error) { + if sid, ok := opts.SourceSteamID(ctx); ok { + opts.Sid64 = sid.Int64() + } + + if opts.CIDR != "" { + if !strings.Contains(opts.CIDR, "/") { + opts.CIDR += "/32" + } + + _, network, errNetwork := net.ParseCIDR(opts.CIDR) + if errNetwork != nil { + slog.Error("Received malformed CIDR", log.ErrAttr(errNetwork)) + + return nil, 0, domain.ErrInvalidCIDR + } + + opts.Network = network.String() + } + + if !(opts.Sid64 > 0 || opts.Network != "") { + return nil, 0, domain.ErrMissingParam + } + + return u.nr.QueryConnections(ctx, opts) } -func (u networkUsecase) QueryConnectionBySteamID(ctx context.Context, opts domain.ConnectionHistoryBySteamIDQueryFilter) ([]domain.PersonConnection, int64, error) { - return u.nr.QueryConnectionBySteamID(ctx, opts) +func (u networkUsecase) QueryNetwork(ctx context.Context, address netip.Addr) (domain.NetworkDetails, error) { + var details domain.NetworkDetails + + if !address.IsValid() { + return details, domain.ErrNetworkInvalidIP + } + + location, errLocation := u.nr.GetLocationRecord(ctx, address) + if errLocation != nil { + return details, errors.Join(errLocation, domain.ErrNetworkLocationUnknown) + } + + details.Location = location + + asn, errASN := u.nr.GetASNRecordByIP(ctx, address) + if errASN != nil { + return details, errors.Join(errASN, domain.ErrNetworkASNUnknown) + } + + details.Asn = asn + + proxy, errProxy := u.nr.GetProxyRecord(ctx, address) + if errProxy != nil && !errors.Is(errProxy, domain.ErrNoResult) { + return details, errors.Join(errProxy, domain.ErrNetworkProxyUnknown) + } + + details.Proxy = proxy + + return details, nil } diff --git a/internal/person/person_repository.go b/internal/person/person_repository.go index e6dda99fe..28fe7247a 100644 --- a/internal/person/person_repository.go +++ b/internal/person/person_repository.go @@ -272,7 +272,7 @@ func (r *personRepository) GetPeople(ctx context.Context, filter domain.PlayerQu if filter.IP != "" { addr := net.ParseIP(filter.IP) if addr == nil { - return nil, 0, domain.ErrInvalidIP + return nil, 0, domain.ErrNetworkInvalidIP } foundIds, errFoundIds := r.GetSteamsAtAddress(ctx, addr) diff --git a/internal/srcds/srcds_service.go b/internal/srcds/srcds_service.go index 145fc01d1..8226226ce 100644 --- a/internal/srcds/srcds_service.go +++ b/internal/srcds/srcds_service.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" "log/slog" - "net" "net/http" + "net/netip" "time" "github.com/gin-gonic/gin" @@ -15,7 +15,6 @@ import ( "github.com/leighmacdonald/gbans/internal/domain" "github.com/leighmacdonald/gbans/internal/httphelper" "github.com/leighmacdonald/gbans/internal/thirdparty" - "github.com/leighmacdonald/gbans/pkg/ip2location" "github.com/leighmacdonald/gbans/pkg/log" "github.com/leighmacdonald/gbans/pkg/util" "github.com/leighmacdonald/steamid/v4/steamid" @@ -328,10 +327,10 @@ func (s *srcdsHandler) onAPIPostPingMod() gin.HandlerFunc { } type CheckRequest struct { - ClientID int `json:"client_id"` - SteamID string `json:"steam_id"` - IP net.IP `json:"ip"` - Name string `json:"name,omitempty"` + ClientID int `json:"client_id"` + SteamID string `json:"steam_id"` + IP netip.Addr `json:"ip"` + Name string `json:"name,omitempty"` } type CheckResponse struct { @@ -499,31 +498,31 @@ func (s *srcdsHandler) onAPIPostServerCheck() gin.HandlerFunc { } } -func (s *srcdsHandler) checkASN(ctx *gin.Context, steamID steamid.SteamID, addr net.IP, responseCtx context.Context, resp *CheckResponse) bool { - var asnRecord ip2location.ASNRecord - - errASN := s.networkUsecase.GetASNRecordByIP(responseCtx, addr, &asnRecord) - if errASN == nil { +func (s *srcdsHandler) checkASN(ctx *gin.Context, steamID steamid.SteamID, addr netip.Addr, responseCtx context.Context, resp *CheckResponse) bool { + details, errDetails := s.networkUsecase.QueryNetwork(ctx, addr) + if errDetails == nil && details.Asn.ASNum > 0 { var asnBan domain.BanASN - if errASNBan := s.banASNUsecase.GetByASN(responseCtx, int64(asnRecord.ASNum), &asnBan); errASNBan != nil { + if errASNBan := s.banASNUsecase.GetByASN(responseCtx, int64(details.Asn.ASNum), &asnBan); errASNBan != nil { if !errors.Is(errASNBan, domain.ErrNoResult) { slog.Error("Failed to fetch asn bannedPerson", log.ErrAttr(errASNBan)) } - } else { - resp.BanType = domain.Banned - resp.Msg = asnBan.Reason.String() - ctx.JSON(http.StatusOK, resp) - slog.Info("Player dropped", slog.String("drop_type", "asn"), - slog.Int64("sid64", steamID.Int64())) - - return true + // Fail open + return false } + + resp.BanType = domain.Banned + resp.Msg = asnBan.Reason.String() + ctx.JSON(http.StatusOK, resp) + slog.Info("Player dropped", slog.String("drop_type", "asn"), + slog.Int64("sid64", steamID.Int64())) + + return true } return false } -func (s *srcdsHandler) checkIPBan(ctx *gin.Context, steamID steamid.SteamID, addr net.IP, responseCtx context.Context, resp *CheckResponse) bool { +func (s *srcdsHandler) checkIPBan(ctx *gin.Context, steamID steamid.SteamID, addr netip.Addr, responseCtx context.Context, resp *CheckResponse) bool { // Check IP first banNet, errGetBanNet := s.banNetUsecase.GetByAddress(responseCtx, addr) if errGetBanNet != nil { @@ -551,7 +550,7 @@ func (s *srcdsHandler) checkIPBan(ctx *gin.Context, steamID steamid.SteamID, add return false } -func (s *srcdsHandler) checkNetBlockBan(ctx *gin.Context, steamID steamid.SteamID, addr net.IP, resp *CheckResponse) bool { +func (s *srcdsHandler) checkNetBlockBan(ctx *gin.Context, steamID steamid.SteamID, addr netip.Addr, resp *CheckResponse) bool { if source, cidrBanned := s.networkUsecase.IsMatch(addr); cidrBanned { resp.BanType = domain.Network resp.Msg = "Network Range Banned.\nIf you using a VPN try disabling it"