github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4clock/matcher.go (about)

     1  // Copyright 2024 VNXME
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package l4clock
    16  
    17  import (
    18  	"strings"
    19  	"time"
    20  	_ "time/tzdata"
    21  
    22  	"github.com/caddyserver/caddy/v2"
    23  	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
    24  
    25  	"github.com/mholt/caddy-l4/layer4"
    26  )
    27  
    28  func init() {
    29  	caddy.RegisterModule(&MatchClock{})
    30  }
    31  
    32  // MatchClock is able to match any connections using the time when they are wrapped/matched.
    33  type MatchClock struct {
    34  	// After is a mandatory field that must have a value in 15:04:05 format representing the lowest valid time point.
    35  	// Placeholders are supported and evaluated at provision. If Before is lower than After, their values are swapped
    36  	// at provision.
    37  	After string `json:"after,omitempty"`
    38  	// Before is a mandatory field that must have a value in 15:04:05 format representing the highest valid time point
    39  	// plus one second. Placeholders are supported and evaluated at provision. 00:00:00 is treated here as 24:00:00.
    40  	// If Before is lower than After, their values are swapped at provision.
    41  	Before string `json:"before,omitempty"`
    42  	// Timezone is an optional field that may be an IANA time zone location (e.g. America/Los_Angeles), a fixed offset
    43  	// to the east of UTC (e.g. +02, -03:30, or even +12:34:56) or Local (to use the system's local time zone).
    44  	// If Timezone is empty, UTC is used by default.
    45  	Timezone string `json:"timezone,omitempty"`
    46  
    47  	location      *time.Location
    48  	secondsAfter  int
    49  	secondsBefore int
    50  }
    51  
    52  // CaddyModule returns the Caddy module information.
    53  func (m *MatchClock) CaddyModule() caddy.ModuleInfo {
    54  	return caddy.ModuleInfo{
    55  		ID:  "layer4.matchers.clock",
    56  		New: func() caddy.Module { return new(MatchClock) },
    57  	}
    58  }
    59  
    60  // Match returns true if the connection wrapping/matching occurs within m's time points.
    61  func (m *MatchClock) Match(cx *layer4.Connection) (bool, error) {
    62  	repl := cx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
    63  	t, known := repl.Get(timeKey)
    64  	if !known {
    65  		t = time.Now().UTC()
    66  		repl.Set(timeKey, t)
    67  	}
    68  	secondsNow := timeToSeconds(t.(time.Time).In(m.location))
    69  	if secondsNow >= m.secondsAfter && secondsNow < m.secondsBefore {
    70  		return true, nil
    71  	}
    72  	return false, nil
    73  }
    74  
    75  // Provision parses m's time points and a time zone (UTC is used by default).
    76  func (m *MatchClock) Provision(_ caddy.Context) (err error) {
    77  	repl := caddy.NewReplacer()
    78  
    79  	after := repl.ReplaceAll(m.After, "")
    80  	if m.secondsAfter, err = timeParseSeconds(after, 0); err != nil {
    81  		return
    82  	}
    83  
    84  	before := repl.ReplaceAll(m.Before, "")
    85  	if m.secondsBefore, err = timeParseSeconds(before, 0); err != nil {
    86  		return
    87  	}
    88  
    89  	// Treat secondsBefore of 00:00:00 as 24:00:00
    90  	if m.secondsBefore == 0 {
    91  		m.secondsBefore = 86400
    92  	}
    93  
    94  	// Swap time points, if secondsAfter is greater than secondsBefore
    95  	if m.secondsBefore < m.secondsAfter {
    96  		m.secondsAfter, m.secondsBefore = m.secondsBefore, m.secondsAfter
    97  	}
    98  
    99  	timezone := repl.ReplaceAll(m.Timezone, "")
   100  	for _, layout := range tzLayouts {
   101  		if len(layout) != len(timezone) {
   102  			continue
   103  		}
   104  		if t, e := time.Parse(layout, timezone); e == nil {
   105  			_, offset := t.Zone()
   106  			m.location = time.FixedZone(timezone, offset)
   107  			break
   108  		}
   109  	}
   110  	if m.location == nil {
   111  		if m.location, err = time.LoadLocation(timezone); err != nil {
   112  			return
   113  		}
   114  	}
   115  
   116  	return nil
   117  }
   118  
   119  // UnmarshalCaddyfile sets up the MatchClock from Caddyfile tokens. Syntax:
   120  //
   121  //	clock <time_after> <time_before> [<time_zone>]
   122  //	clock <after|from> <time_after> [<time_zone>]
   123  //	clock <before|till|to|until> <time_before> [<time_zone>]
   124  //
   125  // Note: MatchClock checks if time_now is greater than or equal to time_after AND less than time_before.
   126  // The lowest value is 00:00:00. If time_before equals 00:00:00, it is treated as 24:00:00. If time_after is greater
   127  // than time_before, they are swapped. Both "after 00:00:00" and "before 00:00:00" match all day. An IANA time zone
   128  // location should be used as a value for time_zone. The system's local time zone may be used with "Local" value.
   129  // If time_zone is empty, UTC is used.
   130  func (m *MatchClock) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
   131  	_, wrapper := d.Next(), d.Val() // consume wrapper name
   132  
   133  	// Only two or three same-line arguments are supported
   134  	if d.CountRemainingArgs() < 2 || d.CountRemainingArgs() > 3 {
   135  		return d.ArgErr()
   136  	}
   137  
   138  	_, first, _, second := d.NextArg(), d.Val(), d.NextArg(), d.Val()
   139  	switch strings.ToLower(first) {
   140  	case "before", "till", "to", "until":
   141  		first = timeMin
   142  		break
   143  	case "after", "from":
   144  		first = timeMax
   145  		second, first = first, second
   146  		break
   147  	}
   148  	m.After, m.Before = first, second
   149  
   150  	if d.NextArg() {
   151  		m.Timezone = d.Val()
   152  	}
   153  
   154  	// No blocks are supported
   155  	if d.NextBlock(d.Nesting()) {
   156  		return d.Errf("malformed %s matcher: blocks are not supported", wrapper)
   157  	}
   158  
   159  	return nil
   160  }
   161  
   162  const (
   163  	timeKey    = "l4.conn.wrap_time"
   164  	timeLayout = time.TimeOnly
   165  	timeMax    = "00:00:00"
   166  	timeMin    = "00:00:00"
   167  )
   168  
   169  var tzLayouts = [...]string{"-07", "-07:00", "-07:00:00"}
   170  
   171  // Interface guards
   172  var (
   173  	_ caddy.Provisioner     = (*MatchClock)(nil)
   174  	_ caddyfile.Unmarshaler = (*MatchClock)(nil)
   175  	_ layer4.ConnMatcher    = (*MatchClock)(nil)
   176  )
   177  
   178  // timeToSeconds gets time and returns the number of seconds passed from the beginning of the current day.
   179  func timeToSeconds(t time.Time) int {
   180  	hh, mm, ss := t.Clock()
   181  	return hh*3600 + mm*60 + ss
   182  }
   183  
   184  // timeParseSeconds parses time string and returns seconds passed from the beginning of the current day.
   185  func timeParseSeconds(src string, def int) (int, error) {
   186  	if len(src) == 0 {
   187  		return def, nil
   188  	}
   189  	t, err := time.Parse(timeLayout, src)
   190  	if err != nil {
   191  		return def, err
   192  	}
   193  	return timeToSeconds(t), nil
   194  }