geo locate user IPs

pull/83/head
Thibault Duplessis 2014-04-29 11:33:50 +02:00
parent 4daca04896
commit 90db51f9aa
8 changed files with 104 additions and 25 deletions

View File

@ -104,12 +104,22 @@
</table>
}
<div class="listings clearfix">
<div style="float: left; margin-left: 1%; width: 21%;">
<strong>@spy.ips.size IP addresses</strong> <ul>@spy.ips.sorted.map { ip =>
<li@{ip._2.??(Html(" class='blocked'"))}>@ip._1</li>
}</ul>
<div class="spy_ips">
<strong>@spy.ips.size IP addresses</strong> <ul>@spy.ipsByLocations.map {
case (location, ips) => {
<li>
<p>@location</p>
<ul>
@ips.map { ip =>
<li@if(ip.blocked) { class="blocked" }>@ip.ip</li>
}
</ul>
</li>
}
}
</ul>
</div>
<div style="margin-left: 24%;">
<div class="spy_uas">
<strong>@spy.uas.size User agent(s)</strong> <ul>@spy.uas.sorted.map { ua =>
<li>@ua</li>
}</ul>

8
bin/gen/geoip 100755
View File

@ -0,0 +1,8 @@
#!/bin/sh
mkdir -p data
cd data
wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz
gunzip GeoLiteCity.dat.gz
rm GeoLiteCity.dat.gz
cd -

View File

@ -24,6 +24,8 @@ final class Env(
val FirewallCollectionFirewall = config getString "firewall.collection.firewall"
val FirewallCachedIpsTtl = config duration "firewall.cached.ips.ttl"
val FloodDuration = config duration "flood.duration"
val GeoIPFile = config getString "geoip.file"
val GeoIPCacheSize = config getInt "geoip.cache_size"
}
import settings._
@ -40,7 +42,11 @@ final class Env(
lazy val forms = new DataForm(captcher = captcher)
lazy val userSpy = UserSpy(firewall) _
private lazy val geoIP = new GeoIP(
file = new java.io.File(GeoIPFile),
cacheSize = GeoIPCacheSize)
lazy val userSpy = UserSpy(firewall, geoIP) _
lazy val disconnect = Store disconnect _

View File

@ -0,0 +1,36 @@
package lila.security
import com.snowplowanalytics.maxmind.geoip.{ IpGeo, IpLocation }
import lila.memo.AsyncCache
import scala.concurrent.Future
private[security] final class GeoIP(file: java.io.File, cacheSize: Int) {
private val ipgeo = new IpGeo(dbFile = file, memCache = false, lruCache = 0)
private val cache = AsyncCache(
f = (ip: String) => Future { ipgeo getLocation ip },
maxCapacity = cacheSize)
def apply(ip: String): Future[Location] =
cache(ip) map (_.fold(Location.unknown)(Location.apply))
}
case class Location(
country: String,
region: Option[String],
city: Option[String]) {
def comparable = (country, ~region, ~city)
override def toString = List(city, region, country.some).flatten mkString ", "
}
object Location {
val unknown = Location("Solar System", none, none)
def apply(ipLoc: IpLocation): Location =
Location(ipLoc.countryName, ipLoc.region, ipLoc.city)
}

View File

@ -14,25 +14,33 @@ import lila.user.{ User, UserRepo }
import tube.storeTube
case class UserSpy(
ips: List[(String, Boolean)],
ips: List[UserSpy.IPData],
uas: List[String],
otherUsers: List[User]) {
def ipStrings = ips map (_._1)
def ipStrings = ips map (_.ip)
def ipsByLocations: List[(Location, List[UserSpy.IPData])] =
ips.sortBy(_.ip).groupBy(_.location).toList.sortBy(_._1.comparable)
}
private[security] object UserSpy {
object UserSpy {
type IP = String
private[security] def apply(firewall: Firewall)(userId: String): Fu[UserSpy] = for {
case class IPData(ip: IP, blocked: Boolean, location: Location)
private[security] def apply(firewall: Firewall, geoIP: GeoIP)(userId: String): Fu[UserSpy] = for {
user UserRepo named userId flatten "[spy] user not found"
objs $find(Json.obj("user" -> user.id))
users explore(Set(user), Set.empty, Set(user))
ips = objs.map(_ str "ip").flatten.distinct
ips = objs.flatMap(_ str "ip").distinct
blockedIps (ips map firewall.blocksIp).sequenceFu
locations <- (ips map geoIP.apply).sequenceFu
users explore(Set(user), Set.empty, Set(user))
} yield UserSpy(
ips = ips zip blockedIps,
ips = ips zip blockedIps zip locations map {
case ((ip, blocked), location) => IPData(ip, blocked, location)
},
uas = objs.map(_ str "ua").flatten.distinct,
otherUsers = (users + user).toList.sortBy(_.createdAt)
)

View File

@ -13,7 +13,7 @@ object ApplicationBuild extends Build {
libraryDependencies ++= Seq(
scalaz, scalalib, hasher, config, apache, scalaTime,
csv, jgit, actuarius, elastic4s, findbugs, RM,
PRM, spray.caching),
PRM, spray.caching, maxmind),
scalacOptions := compilerOptions,
sources in doc in Compile := List(),
incOptions := incOptions.value.withNameHashing(true),
@ -145,7 +145,7 @@ object ApplicationBuild extends Build {
lazy val security = project("security", Seq(common, hub, db, user)).settings(
libraryDependencies ++= provided(
play.api, RM, PRM, spray.caching)
play.api, RM, PRM, maxmind)
)
lazy val relation = project("relation", Seq(common, db, memo, hub, user, game)).settings(

View File

@ -45,6 +45,8 @@ object Dependencies {
val elastic4s = "com.sksamuel.elastic4s" %% "elastic4s" % "1.0.1.0"
val RM = "org.reactivemongo" %% "reactivemongo" % "0.10.0"
val PRM = "org.reactivemongo" %% "play2-reactivemongo" % "0.10.2"
val maxmind = "com.snowplowanalytics" %% "scala-maxmind-geoip" % "0.0.5"
object play {
val version = "2.2.1"
val api = "com.typesafe.play" %% "play" % version

View File

@ -41,16 +41,13 @@ div.user_show .relation_actions {
div.user_show .relation_actions form {
display: inline;
}
div.user_show div.meat {
height: 325px;
position: relative;
}
div.engine_warning {
margin: 10px 0;
}
div.sub_ratings.sep {
margin-top: 2.5em;
}
@ -62,7 +59,6 @@ div.sub_ratings h3 {
#site_header div.sub_ratings strong {
font-weight: bold;
}
div.user_show .rating_history {
position: absolute;
top: -10px;
@ -76,7 +72,6 @@ div.user_show .rating_history span {
top: 164px;
left: 232px;
}
div.user_show .user-infos {
position: absolute;
top: 0;
@ -87,7 +82,6 @@ div.user_show .user-infos {
width: 310px;
height: 325px;
}
div.user_show .name,
div.user_show .bio,
div.user_show .boxed_data,
@ -100,12 +94,10 @@ div.user_show .teams {
div.user_show .bio {
font-style: italic;
}
div.user_show .teams a {
display: block;
margin: 5px 0;
}
div.user_show div.boxed_data {
width: 292px;
padding: 5px 8px;
@ -117,11 +109,9 @@ div.user_show div.boxed_data a.user_link {
font-weight: bold;
text-decoration: none;
}
div.user_show div.games {
margin-top: 10px;
}
div.user_show .profile {
margin-bottom: 1em;
}
@ -179,6 +169,25 @@ div.user_show .mod_zone .listings > div {
max-height: 20em;
overflow: auto;
}
div.user_show .mod_zone .spy_ips {
float: left;
margin-left: 1%;
width: 31%;
}
div.user_show .mod_zone .spy_ips > ul > li {
list-style: inside disc;
}
div.user_show .mod_zone .spy_ips ul p {
font-weight: bold;
display: inline;
}
div.user_show .mod_zone .spy_ips li li {
margin-left: 1em;
font-family: monospace;
}
div.user_show .mod_zone .spy_uas {
margin-left: 34%;
}
div.user_show .mod_zone .listings .blocked {
color: red;
font-weight: bold;