1   /*
2    * Copyright 2017 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package com.linecorp.centraldogma.server.internal.thrift;
17  
18  import static java.util.Objects.requireNonNull;
19  
20  import java.net.InetSocketAddress;
21  import java.time.Clock;
22  import java.time.Instant;
23  import java.time.temporal.ChronoUnit;
24  import java.util.concurrent.ConcurrentHashMap;
25  import java.util.concurrent.ConcurrentMap;
26  import java.util.regex.Pattern;
27  
28  import org.slf4j.Logger;
29  import org.slf4j.LoggerFactory;
30  
31  import com.google.common.annotations.VisibleForTesting;
32  
33  import com.linecorp.armeria.common.HttpHeaderNames;
34  import com.linecorp.armeria.common.HttpRequest;
35  import com.linecorp.armeria.common.HttpResponse;
36  import com.linecorp.armeria.server.HttpService;
37  import com.linecorp.armeria.server.ServiceRequestContext;
38  import com.linecorp.armeria.server.SimpleDecoratingHttpService;
39  
40  public class TokenlessClientLogger extends SimpleDecoratingHttpService {
41  
42      private static final Logger logger = LoggerFactory.getLogger(TokenlessClientLogger.class);
43  
44      private static final Pattern PATTERN = Pattern.compile("\\s*[Bb][Ee][Aa][Rr][Ee][Rr]\\s+anonymous\\s*");
45  
46      private final Clock clock;
47      private final ConcurrentMap<String, Instant> reportedAddresses = new ConcurrentHashMap<>();
48  
49      public TokenlessClientLogger(HttpService delegate) {
50          this(delegate, Clock.systemUTC());
51      }
52  
53      @VisibleForTesting
54      TokenlessClientLogger(HttpService delegate, Clock clock) {
55          super(delegate);
56          this.clock = requireNonNull(clock, "clock");
57      }
58  
59      @Override
60      public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {
61          final String authorization = req.headers().get(HttpHeaderNames.AUTHORIZATION);
62          if (authorization == null || !PATTERN.matcher(authorization).matches()) {
63              final InetSocketAddress raddr = ctx.remoteAddress();
64              final String ip = raddr.getAddress().getHostAddress();
65              final Instant now = Instant.now(clock);
66              final Instant lastReport = reportedAddresses.putIfAbsent(ip, now);
67              final boolean report;
68              if (lastReport == null) {
69                  report = true;
70              } else if (ChronoUnit.DAYS.between(lastReport, now) >= 1) {
71                  report = reportedAddresses.replace(ip, lastReport, now);
72              } else {
73                  report = false;
74              }
75  
76              if (report) {
77                  report(raddr.getHostString(), ip);
78              }
79          }
80  
81          return unwrap().serve(ctx, req);
82      }
83  
84      @VisibleForTesting
85      void report(String hostname, String ip) {
86          logger.debug("Received a request without 'authorization' header from: {}/{}", hostname, ip);
87      }
88  }