Skip to content

Commit bd4c887

Browse files
committed
log IP address on failed input or failed login
1 parent ded4250 commit bd4c887

4 files changed

Lines changed: 34 additions & 15 deletions

File tree

src/main/kotlin/ch/derlin/bbdata/common/Beans.kt renamed to src/main/kotlin/ch/derlin/bbdata/common/Misc.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ch.derlin.bbdata.common
22

33
import com.fasterxml.jackson.annotation.JsonCreator
44
import com.fasterxml.jackson.annotation.JsonValue
5+
import javax.servlet.http.HttpServletRequest
56
import javax.validation.Valid
67
import javax.validation.constraints.NotNull
78
import javax.validation.constraints.Size
@@ -56,6 +57,17 @@ class ValidatedList<E> {
5657
}
5758
}
5859

60+
/**
61+
* Get the client's IP from the request
62+
*/
63+
fun HttpServletRequest.getIp(): String {
64+
val ip = this.getHeader("X-Forwarded-For") ?: this.remoteAddr
65+
return if (ip == "0:0:0:0:0:0:0:1") "127.0.0.1" else ip
66+
67+
}
5968

69+
/**
70+
* Ellipsise a string. E.G "1234".truncate(4) = "1234", "1234567".truncate(4) = "1234[...]"
71+
*/
6072
fun String?.truncate(maxLength: Int = 30, ellipsis: String = "[${Typography.ellipsis}]"): String? =
6173
if (this != null && this.length > maxLength) this.take(maxLength).plus(ellipsis) else this

src/main/kotlin/ch/derlin/bbdata/input/InputController.kt

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ch.derlin.bbdata.common.cassandra.RawValueRepository
66
import ch.derlin.bbdata.common.exceptions.ForbiddenException
77
import ch.derlin.bbdata.common.exceptions.ItemNotFoundException
88
import ch.derlin.bbdata.common.exceptions.WrongParamsException
9+
import ch.derlin.bbdata.common.getIp
910
import ch.derlin.bbdata.common.stats.StatsLogic
1011
import ch.derlin.bbdata.output.api.types.BaseType
1112
import com.fasterxml.jackson.databind.ObjectMapper
@@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.PostMapping
1920
import org.springframework.web.bind.annotation.RequestBody
2021
import org.springframework.web.bind.annotation.RequestParam
2122
import org.springframework.web.bind.annotation.RestController
23+
import javax.servlet.http.HttpServletRequest
2224
import javax.validation.Valid
2325
import javax.validation.constraints.NotNull
2426

@@ -46,7 +48,6 @@ class InputController(
4648

4749
private val MAX_LAG: Long = 2000 // in millis
4850

49-
5051
data class NewValueAugmented(
5152
val objectId: Long,
5253
val timestamp: DateTime,
@@ -78,7 +79,8 @@ class InputController(
7879
"If you omit to provide a timestamp for any measure, it will be added automatically (server time). " +
7980
"This request is *atomic*: either *all* measures are valid and saved, or none. ")
8081
fun postNewMeasures(@Valid @NotNull @RequestBody rawMeasures: ValidatedList<NewValue>,
81-
@RequestParam("simulate", defaultValue = "false") sim: Boolean): List<NewValueAugmented> {
82+
@RequestParam("simulate", defaultValue = "false") sim: Boolean,
83+
request: HttpServletRequest): List<NewValueAugmented> {
8284

8385
val now = DateTime()
8486

@@ -89,7 +91,7 @@ class InputController(
8991
val measure = if (rawMeasure.timestamp != null) {
9092
// check that date is in the past
9193
if (rawMeasure.timestamp.millis > now.millis + MAX_LAG) {
92-
log.info("REJECTED: wrong timestamp: ${rawMeasure}")
94+
log.info("REJECTED: <IP:${request.getIp()}> wrong timestamp: ${rawMeasure}")
9395
throw WrongParamsException("objectId ${rawMeasure.objectId}: date should be in the past. input='${rawMeasure.timestamp}', now='$now'")
9496
}
9597
rawMeasure
@@ -100,18 +102,18 @@ class InputController(
100102

101103
// get metadata
102104
val meta = inputRepository.getMeasureMeta(measure.objectId!!, measure.token!!).orElseThrow {
103-
log.info("REJECTED: wrong token: $measure")
105+
log.info("REJECTED: <IP:${request.getIp()}> wrong token: $measure")
104106
ItemNotFoundException(msg = "objectId ${measure.objectId}: the pair <objectId, token> does not exist")
105107
}
106108
// ensure the object is enabled. This is just a double check, as tokens cannot be created on disabled objects
107109
if (meta.disabled) {
108-
log.info("REJECTED: object is disabled: $rawMeasure")
110+
log.info("REJECTED: <IP:${request.getIp()}> object is disabled: $rawMeasure")
109111
throw ForbiddenException("objectId ${rawMeasure.objectId} is disabled.")
110112
}
111113
// ensure the measure matches the type declared, and parse it
112114
val parsedValue = BaseType.parseType(measure.value!!, meta.type)
113-
if(parsedValue == null) {
114-
log.info("REJECTED: wrong value given the unit: $measure, $meta")
115+
if (parsedValue == null) {
116+
log.info("REJECTED: <IP:${request.getIp()}> wrong value given the unit: $measure, $meta")
115117
throw WrongParamsException("objectId ${rawMeasure.objectId}: the value '${measure.value}' does not match " +
116118
"the unit ${meta.unitSymbol} (${meta.type}) declared in the object definition.")
117119
}
@@ -124,14 +126,14 @@ class InputController(
124126

125127
// ensure no duplicate objectId/timestamp in the data sent
126128
if (valueKeys.contains(key)) {
127-
log.info("REJECTED: duplicate in body: $measure")
129+
log.info("REJECTED: <IP:${request.getIp()}> duplicate in body: $measure")
128130
throw WrongParamsException("objectId ${measure.objectId}: two or more values with the same timestamp")
129131
}
130132
valueKeys.add(key)
131133

132134
// ensure it doesn't already exist in cassandra TODO: find a better way ?
133135
if (rawValueRepository.existsById(rawValue.key)) {
134-
log.info("REJECTED: duplicate in cassandra: $measure")
136+
log.info("REJECTED: <IP:${request.getIp()}> duplicate in cassandra: $measure")
135137
throw WrongParamsException("objectId ${rawMeasure.objectId}: " +
136138
"a value with the same timestamp (${measure.timestamp}) already exists for this object.")
137139
}

src/main/kotlin/ch/derlin/bbdata/input/InputControllerDeprecated.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.PostMapping
77
import org.springframework.web.bind.annotation.RequestBody
88
import org.springframework.web.bind.annotation.RequestParam
99
import org.springframework.web.bind.annotation.RestController
10+
import javax.servlet.http.HttpServletRequest
1011
import javax.validation.Valid
1112
import javax.validation.constraints.NotNull
1213

@@ -25,13 +26,15 @@ class InputControllerDeprecated(
2526
@Operation(description = "**DEPRECATED**: this endpoint may disappear in newer versions. Please use `/objects/values` instead. <br>" +
2627
"Submit a new measure. This is similar to `/objects/values`, but takes only one measure in the body.")
2728
fun postNewMeasure(@Valid @NotNull @RequestBody rawMeasure: NewValue,
28-
@RequestParam("simulate", defaultValue = "false") sim: Boolean): InputController.NewValueAugmented =
29-
inputController.postNewMeasures(ValidatedList(rawMeasure), sim)[0]
29+
@RequestParam("simulate", defaultValue = "false") sim: Boolean,
30+
request: HttpServletRequest): InputController.NewValueAugmented =
31+
inputController.postNewMeasures(ValidatedList(rawMeasure), sim, request)[0]
3032

3133
@PostMapping("input/measures/bulk")
3234
@Operation(description = "**DEPRECATED**: this endpoint may disappear in newer versions. Please use `/objects/values` instead.")
3335
fun postNewMeasureBulk(@Valid @NotNull @RequestBody rawMeasures: ValidatedList<NewValue>,
34-
@RequestParam("simulate", defaultValue = "false") sim: Boolean): List<InputController.NewValueAugmented> =
35-
inputController.postNewMeasures(rawMeasures, sim)
36+
@RequestParam("simulate", defaultValue = "false") sim: Boolean,
37+
request: HttpServletRequest): List<InputController.NewValueAugmented> =
38+
inputController.postNewMeasures(rawMeasures, sim, request)
3639

3740
}

src/main/kotlin/ch/derlin/bbdata/output/api/apikeys/ApikeyController.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ch.derlin.bbdata.common.dates.DurationParser
55
import ch.derlin.bbdata.common.dates.JodaUtils
66
import ch.derlin.bbdata.common.exceptions.ForbiddenException
77
import ch.derlin.bbdata.common.exceptions.ItemNotFoundException
8+
import ch.derlin.bbdata.common.getIp
89
import ch.derlin.bbdata.output.api.CommonResponses
910
import ch.derlin.bbdata.output.api.SimpleModificationStatusResponse
1011
import ch.derlin.bbdata.output.api.users.UserRepository
@@ -19,6 +20,7 @@ import org.slf4j.Logger
1920
import org.slf4j.LoggerFactory
2021
import org.springframework.http.ResponseEntity
2122
import org.springframework.web.bind.annotation.*
23+
import javax.servlet.http.HttpServletRequest
2224
import javax.validation.Valid
2325
import javax.validation.constraints.NotNull
2426
import javax.validation.constraints.Size
@@ -57,7 +59,7 @@ class ApikeyController(
5759
@Operation(description = "Login to the API using username/password. " +
5860
"It will create and return a writable apikey valid for $AUTOLOGIN_EXPIRE_HOURS hours.<br> " +
5961
"To access the other endpoints, use your user ID and the 32-char apikey secret returned.")
60-
fun login(@Valid @NotNull @RequestBody loginBody: LoginBody): Apikey {
62+
fun login(@Valid @NotNull @RequestBody loginBody: LoginBody, request: HttpServletRequest): Apikey {
6163
val optionalUserId = userRepository.findByName(loginBody.username!!).map { it.id }
6264
if (optionalUserId.isPresent && apikeyRepository.canLogin(optionalUserId.get(), loginBody.password!!) > 0) {
6365
return apikeyRepository.saveAndFlush(Apikey(
@@ -68,7 +70,7 @@ class ApikeyController(
6870
expirationDate = DateTime().plus(AUTOLOGIN_EXPIRE)
6971
))
7072
}
71-
log.info("invalid login for username='${loginBody.username}' password='${loginBody.password}'")
73+
log.info("FAILED LOGIN: <IP:${request.getIp()}> username='${loginBody.username}' password='${loginBody.password}'")
7274
throw ForbiddenException("Wrong username or password.")
7375
}
7476

0 commit comments

Comments
 (0)