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 }