2019-07-01 06:07:58 +08:00
|
|
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2019-06-15 01:58:28 +08:00
|
|
|
package caddy
|
2019-05-17 06:05:38 +08:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"reflect"
|
|
|
|
|
2020-03-07 14:15:25 +08:00
|
|
|
"github.com/caddyserver/certmagic"
|
2019-10-29 04:39:37 +08:00
|
|
|
"go.uber.org/zap"
|
2019-05-17 06:05:38 +08:00
|
|
|
)
|
|
|
|
|
2019-05-17 22:48:12 +08:00
|
|
|
// Context is a type which defines the lifetime of modules that
|
|
|
|
// are loaded and provides access to the parent configuration
|
|
|
|
// that spawned the modules which are loaded. It should be used
|
2019-06-27 06:03:29 +08:00
|
|
|
// with care and wrapped with derivation functions from the
|
|
|
|
// standard context package only if you don't need the Caddy
|
2020-01-04 02:33:22 +08:00
|
|
|
// specific features. These contexts are canceled when the
|
2019-10-11 05:38:30 +08:00
|
|
|
// lifetime of the modules loaded from it is over.
|
2019-05-17 22:48:12 +08:00
|
|
|
//
|
|
|
|
// Use NewContext() to get a valid value (but most modules will
|
|
|
|
// not actually need to do this).
|
2019-05-17 06:05:38 +08:00
|
|
|
type Context struct {
|
|
|
|
context.Context
|
|
|
|
moduleInstances map[string][]interface{}
|
|
|
|
cfg *Config
|
2019-05-30 13:10:12 +08:00
|
|
|
cleanupFuncs []func()
|
2019-05-17 06:05:38 +08:00
|
|
|
}
|
|
|
|
|
2019-05-17 22:48:12 +08:00
|
|
|
// NewContext provides a new context derived from the given
|
|
|
|
// context ctx. Normally, you will not need to call this
|
|
|
|
// function unless you are loading modules which have a
|
|
|
|
// different lifespan than the ones for the context the
|
|
|
|
// module was provisioned with. Be sure to call the cancel
|
|
|
|
// func when the context is to be cleaned up so that
|
|
|
|
// modules which are loaded will be properly unloaded.
|
|
|
|
// See standard library context package's documentation.
|
2019-05-17 06:05:38 +08:00
|
|
|
func NewContext(ctx Context) (Context, context.CancelFunc) {
|
|
|
|
newCtx := Context{moduleInstances: make(map[string][]interface{}), cfg: ctx.cfg}
|
|
|
|
c, cancel := context.WithCancel(ctx.Context)
|
|
|
|
wrappedCancel := func() {
|
|
|
|
cancel()
|
2019-05-14 22:35:41 +08:00
|
|
|
|
2019-05-30 13:10:12 +08:00
|
|
|
for _, f := range ctx.cleanupFuncs {
|
|
|
|
f()
|
|
|
|
}
|
|
|
|
|
2019-05-17 06:05:38 +08:00
|
|
|
for modName, modInstances := range newCtx.moduleInstances {
|
|
|
|
for _, inst := range modInstances {
|
|
|
|
if cu, ok := inst.(CleanerUpper); ok {
|
|
|
|
err := cu.Cleanup()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("[ERROR] %s (%p): cleanup: %v", modName, inst, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newCtx.Context = c
|
|
|
|
return newCtx, wrappedCancel
|
|
|
|
}
|
|
|
|
|
2020-01-04 02:33:22 +08:00
|
|
|
// OnCancel executes f when ctx is canceled.
|
2019-05-30 13:10:12 +08:00
|
|
|
func (ctx *Context) OnCancel(f func()) {
|
|
|
|
ctx.cleanupFuncs = append(ctx.cleanupFuncs, f)
|
|
|
|
}
|
|
|
|
|
2019-12-11 04:36:46 +08:00
|
|
|
// LoadModule loads the Caddy module(s) from the specified field of the parent struct
|
|
|
|
// pointer and returns the loaded module(s). The struct pointer and its field name as
|
|
|
|
// a string are necessary so that reflection can be used to read the struct tag on the
|
|
|
|
// field to get the module namespace and inline module name key (if specified).
|
|
|
|
//
|
|
|
|
// The field can be any one of the supported raw module types: json.RawMessage,
|
|
|
|
// []json.RawMessage, map[string]json.RawMessage, or []map[string]json.RawMessage.
|
|
|
|
// ModuleMap may be used in place of map[string]json.RawMessage. The return value's
|
|
|
|
// underlying type mirrors the input field's type:
|
|
|
|
//
|
|
|
|
// json.RawMessage => interface{}
|
|
|
|
// []json.RawMessage => []interface{}
|
|
|
|
// map[string]json.RawMessage => map[string]interface{}
|
|
|
|
// []map[string]json.RawMessage => []map[string]interface{}
|
|
|
|
//
|
|
|
|
// The field must have a "caddy" struct tag in this format:
|
|
|
|
//
|
|
|
|
// caddy:"key1=val1 key2=val2"
|
|
|
|
//
|
|
|
|
// To load modules, a "namespace" key is required. For example, to load modules
|
|
|
|
// in the "http.handlers" namespace, you'd put: `namespace=http.handlers` in the
|
|
|
|
// Caddy struct tag.
|
|
|
|
//
|
|
|
|
// The module name must also be available. If the field type is a map or slice of maps,
|
|
|
|
// then key is assumed to be the module name if an "inline_key" is NOT specified in the
|
|
|
|
// caddy struct tag. In this case, the module name does NOT need to be specified in-line
|
|
|
|
// with the module itself.
|
|
|
|
//
|
|
|
|
// If not a map, or if inline_key is non-empty, then the module name must be embedded
|
|
|
|
// into the values, which must be objects; then there must be a key in those objects
|
|
|
|
// where its associated value is the module name. This is called the "inline key",
|
|
|
|
// meaning the key containing the module's name that is defined inline with the module
|
|
|
|
// itself. You must specify the inline key in a struct tag, along with the namespace:
|
|
|
|
//
|
|
|
|
// caddy:"namespace=http.handlers inline_key=handler"
|
|
|
|
//
|
|
|
|
// This will look for a key/value pair like `"handler": "..."` in the json.RawMessage
|
|
|
|
// in order to know the module name.
|
|
|
|
//
|
|
|
|
// To make use of the loaded module(s) (the return value), you will probably want
|
|
|
|
// to type-assert each interface{} value(s) to the types that are useful to you
|
|
|
|
// and store them on the same struct. Storing them on the same struct makes for
|
|
|
|
// easy garbage collection when your host module is no longer needed.
|
|
|
|
//
|
2019-12-11 05:06:35 +08:00
|
|
|
// Loaded modules have already been provisioned and validated. Upon returning
|
|
|
|
// successfully, this method clears the json.RawMessage(s) in the field since
|
|
|
|
// the raw JSON is no longer needed, and this allows the GC to free up memory.
|
2019-12-11 04:36:46 +08:00
|
|
|
func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (interface{}, error) {
|
|
|
|
val := reflect.ValueOf(structPointer).Elem().FieldByName(fieldName)
|
|
|
|
typ := val.Type()
|
|
|
|
|
|
|
|
field, ok := reflect.TypeOf(structPointer).Elem().FieldByName(fieldName)
|
|
|
|
if !ok {
|
|
|
|
panic(fmt.Sprintf("field %s does not exist in %#v", fieldName, structPointer))
|
|
|
|
}
|
|
|
|
|
|
|
|
opts, err := ParseStructTag(field.Tag.Get("caddy"))
|
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Sprintf("malformed tag on field %s: %v", fieldName, err))
|
|
|
|
}
|
|
|
|
|
|
|
|
moduleNamespace, ok := opts["namespace"]
|
|
|
|
if !ok {
|
|
|
|
panic(fmt.Sprintf("missing 'namespace' key in struct tag on field %s", fieldName))
|
|
|
|
}
|
|
|
|
inlineModuleKey := opts["inline_key"]
|
|
|
|
|
|
|
|
var result interface{}
|
|
|
|
|
|
|
|
switch val.Kind() {
|
|
|
|
case reflect.Slice:
|
|
|
|
if isJSONRawMessage(typ) {
|
|
|
|
// val is `json.RawMessage` ([]uint8 under the hood)
|
|
|
|
|
|
|
|
if inlineModuleKey == "" {
|
|
|
|
panic("unable to determine module name without inline_key when type is not a ModuleMap")
|
|
|
|
}
|
|
|
|
val, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, val.Interface().(json.RawMessage))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result = val
|
|
|
|
|
|
|
|
} else if isJSONRawMessage(typ.Elem()) {
|
|
|
|
// val is `[]json.RawMessage`
|
|
|
|
|
|
|
|
if inlineModuleKey == "" {
|
|
|
|
panic("unable to determine module name without inline_key because type is not a ModuleMap")
|
|
|
|
}
|
|
|
|
var all []interface{}
|
|
|
|
for i := 0; i < val.Len(); i++ {
|
|
|
|
val, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, val.Index(i).Interface().(json.RawMessage))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("position %d: %v", i, err)
|
|
|
|
}
|
|
|
|
all = append(all, val)
|
|
|
|
}
|
|
|
|
result = all
|
|
|
|
|
|
|
|
} else if isModuleMapType(typ.Elem()) {
|
|
|
|
// val is `[]map[string]json.RawMessage`
|
|
|
|
|
|
|
|
var all []map[string]interface{}
|
|
|
|
for i := 0; i < val.Len(); i++ {
|
|
|
|
thisSet, err := ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val.Index(i))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
all = append(all, thisSet)
|
|
|
|
}
|
|
|
|
result = all
|
|
|
|
}
|
|
|
|
|
|
|
|
case reflect.Map:
|
|
|
|
// val is a ModuleMap or some other kind of map
|
|
|
|
result, err = ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("unrecognized type for module: %s", typ)
|
|
|
|
}
|
|
|
|
|
|
|
|
// we're done with the raw bytes; allow GC to deallocate
|
|
|
|
val.Set(reflect.Zero(typ))
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]interface{}.
|
2020-02-28 10:30:48 +08:00
|
|
|
// Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module
|
2019-12-11 04:36:46 +08:00
|
|
|
// name) or as a regular map (key is not the module name, and module name is defined inline).
|
|
|
|
func (ctx Context) loadModulesFromSomeMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]interface{}, error) {
|
|
|
|
// if no inline_key is specified, then val must be a ModuleMap,
|
|
|
|
// where the key is the module name
|
|
|
|
if inlineModuleKey == "" {
|
|
|
|
if !isModuleMapType(val.Type()) {
|
|
|
|
panic(fmt.Sprintf("expected ModuleMap because inline_key is empty; but we do not recognize this type: %s", val.Type()))
|
|
|
|
}
|
|
|
|
return ctx.loadModuleMap(namespace, val)
|
|
|
|
}
|
|
|
|
|
|
|
|
// otherwise, val is a map with modules, but the module name is
|
|
|
|
// inline with each value (the key means something else)
|
|
|
|
return ctx.loadModulesFromRegularMap(namespace, inlineModuleKey, val)
|
|
|
|
}
|
|
|
|
|
|
|
|
// loadModulesFromRegularMap loads modules from val, where val is a map[string]json.RawMessage.
|
|
|
|
// Map keys are NOT interpreted as module names, so module names are still expected to appear
|
|
|
|
// inline with the objects.
|
|
|
|
func (ctx Context) loadModulesFromRegularMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]interface{}, error) {
|
|
|
|
mods := make(map[string]interface{})
|
|
|
|
iter := val.MapRange()
|
|
|
|
for iter.Next() {
|
|
|
|
k := iter.Key()
|
|
|
|
v := iter.Value()
|
|
|
|
mod, err := ctx.loadModuleInline(inlineModuleKey, namespace, v.Interface().(json.RawMessage))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("key %s: %v", k, err)
|
|
|
|
}
|
|
|
|
mods[k.String()] = mod
|
|
|
|
}
|
|
|
|
return mods, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// loadModuleMap loads modules from a ModuleMap, i.e. map[string]interface{}, where the key is the
|
|
|
|
// module name. With a module map, module names do not need to be defined inline with their values.
|
|
|
|
func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[string]interface{}, error) {
|
|
|
|
all := make(map[string]interface{})
|
|
|
|
iter := val.MapRange()
|
|
|
|
for iter.Next() {
|
|
|
|
k := iter.Key().Interface().(string)
|
|
|
|
v := iter.Value().Interface().(json.RawMessage)
|
|
|
|
moduleName := namespace + "." + k
|
|
|
|
if namespace == "" {
|
|
|
|
moduleName = k
|
|
|
|
}
|
|
|
|
val, err := ctx.LoadModuleByID(moduleName, v)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("module name '%s': %v", k, err)
|
|
|
|
}
|
|
|
|
all[k] = val
|
|
|
|
}
|
|
|
|
return all, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// LoadModuleByID decodes rawMsg into a new instance of mod and
|
|
|
|
// returns the value. If mod.New is nil, an error is returned.
|
|
|
|
// If the module implements Validator or Provisioner interfaces,
|
|
|
|
// those methods are invoked to ensure the module is fully
|
|
|
|
// configured and valid before being used.
|
|
|
|
//
|
|
|
|
// This is a lower-level method and will usually not be called
|
|
|
|
// directly by most modules. However, this method is useful when
|
|
|
|
// dynamically loading/unloading modules in their own context,
|
|
|
|
// like from embedded scripts, etc.
|
|
|
|
func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{}, error) {
|
|
|
|
modulesMu.RLock()
|
|
|
|
mod, ok := modules[id]
|
|
|
|
modulesMu.RUnlock()
|
2019-05-17 06:05:38 +08:00
|
|
|
if !ok {
|
2019-12-11 04:36:46 +08:00
|
|
|
return nil, fmt.Errorf("unknown module: %s", id)
|
2019-05-17 06:05:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if mod.New == nil {
|
2019-12-11 04:36:46 +08:00
|
|
|
return nil, fmt.Errorf("module '%s' has no constructor", mod.ID)
|
2019-05-17 06:05:38 +08:00
|
|
|
}
|
|
|
|
|
2019-08-22 00:46:35 +08:00
|
|
|
val := mod.New().(interface{})
|
2019-05-17 06:05:38 +08:00
|
|
|
|
2019-08-22 00:46:35 +08:00
|
|
|
// value must be a pointer for unmarshaling into concrete type, even if
|
|
|
|
// the module's concrete type is a slice or map; New() *should* return
|
|
|
|
// a pointer, otherwise unmarshaling errors or panics will occur
|
2019-05-17 06:05:38 +08:00
|
|
|
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr {
|
2019-08-22 00:46:35 +08:00
|
|
|
log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+
|
|
|
|
" so we are using reflection to make a pointer instead; please fix this by"+
|
2019-12-11 04:36:46 +08:00
|
|
|
" using new(Type) or &Type notation in your module's New() function.", id)
|
2019-08-22 00:46:35 +08:00
|
|
|
val = reflect.New(rv.Type()).Elem().Addr().Interface().(Module)
|
2019-05-17 06:05:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// fill in its config only if there is a config to fill in
|
|
|
|
if len(rawMsg) > 0 {
|
2019-05-23 04:32:12 +08:00
|
|
|
err := strictUnmarshalJSON(rawMsg, &val)
|
2019-05-17 06:05:38 +08:00
|
|
|
if err != nil {
|
2019-12-11 04:36:46 +08:00
|
|
|
return nil, fmt.Errorf("decoding module config: %s: %v", mod, err)
|
2019-05-17 06:05:38 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-27 00:45:34 +08:00
|
|
|
if val == nil {
|
|
|
|
// returned module values are almost always type-asserted
|
|
|
|
// before being used, so a nil value would panic; and there
|
|
|
|
// is no good reason to explicitly declare null modules in
|
2019-12-11 04:36:46 +08:00
|
|
|
// a config; it might be because the user is trying to achieve
|
|
|
|
// a result the developer isn't expecting, which is a smell
|
2019-06-27 00:45:34 +08:00
|
|
|
return nil, fmt.Errorf("module value cannot be null")
|
|
|
|
}
|
|
|
|
|
2019-05-17 06:05:38 +08:00
|
|
|
if prov, ok := val.(Provisioner); ok {
|
|
|
|
err := prov.Provision(ctx)
|
|
|
|
if err != nil {
|
2019-09-30 23:16:01 +08:00
|
|
|
// incomplete provisioning could have left state
|
|
|
|
// dangling, so make sure it gets cleaned up
|
|
|
|
if cleanerUpper, ok := val.(CleanerUpper); ok {
|
|
|
|
err2 := cleanerUpper.Cleanup()
|
|
|
|
if err2 != nil {
|
|
|
|
err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
|
|
|
|
}
|
|
|
|
}
|
2019-12-11 04:36:46 +08:00
|
|
|
return nil, fmt.Errorf("provision %s: %v", mod, err)
|
2019-05-17 06:05:38 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if validator, ok := val.(Validator); ok {
|
2019-05-17 22:48:12 +08:00
|
|
|
err := validator.Validate()
|
2019-05-17 06:05:38 +08:00
|
|
|
if err != nil {
|
2019-09-30 23:16:01 +08:00
|
|
|
// since the module was already provisioned, make sure we clean up
|
2019-05-17 06:05:38 +08:00
|
|
|
if cleanerUpper, ok := val.(CleanerUpper); ok {
|
|
|
|
err2 := cleanerUpper.Cleanup()
|
|
|
|
if err2 != nil {
|
|
|
|
err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
|
|
|
|
}
|
|
|
|
}
|
2019-12-11 04:36:46 +08:00
|
|
|
return nil, fmt.Errorf("%s: invalid configuration: %v", mod, err)
|
2019-05-17 06:05:38 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-11 04:36:46 +08:00
|
|
|
ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val)
|
2019-05-17 06:05:38 +08:00
|
|
|
|
|
|
|
return val, nil
|
|
|
|
}
|
|
|
|
|
2019-12-11 04:36:46 +08:00
|
|
|
// loadModuleInline loads a module from a JSON raw message which decodes to
|
|
|
|
// a map[string]interface{}, where one of the object keys is moduleNameKey
|
|
|
|
// and the corresponding value is the module name (as a string) which can
|
|
|
|
// be found in the given scope. In other words, the module name is declared
|
|
|
|
// in-line with the module itself.
|
|
|
|
//
|
|
|
|
// This allows modules to be decoded into their concrete types and used when
|
|
|
|
// their names cannot be the unique key in a map, such as when there are
|
|
|
|
// multiple instances in the map or it appears in an array (where there are
|
|
|
|
// no custom keys). In other words, the key containing the module name is
|
|
|
|
// treated special/separate from all the other keys in the object.
|
|
|
|
func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (interface{}, error) {
|
2019-05-23 04:32:12 +08:00
|
|
|
moduleName, raw, err := getModuleNameInline(moduleNameKey, raw)
|
2019-05-17 06:05:38 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-12-11 04:36:46 +08:00
|
|
|
val, err := ctx.LoadModuleByID(moduleScope+"."+moduleName, raw)
|
2019-05-17 06:05:38 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("loading module '%s': %v", moduleName, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return val, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// App returns the configured app named name. If no app with
|
|
|
|
// that name is currently configured, a new empty one will be
|
|
|
|
// instantiated. (The app module must still be registered.)
|
|
|
|
func (ctx Context) App(name string) (interface{}, error) {
|
|
|
|
if app, ok := ctx.cfg.apps[name]; ok {
|
|
|
|
return app, nil
|
|
|
|
}
|
2020-03-07 14:15:25 +08:00
|
|
|
appRaw := ctx.cfg.AppsRaw[name]
|
|
|
|
modVal, err := ctx.LoadModuleByID(name, appRaw)
|
2019-05-17 06:05:38 +08:00
|
|
|
if err != nil {
|
2020-03-07 14:15:25 +08:00
|
|
|
return nil, fmt.Errorf("loading %s app module: %v", name, err)
|
|
|
|
}
|
|
|
|
if appRaw != nil {
|
|
|
|
ctx.cfg.AppsRaw[name] = nil // allow GC to deallocate
|
2019-05-17 06:05:38 +08:00
|
|
|
}
|
|
|
|
ctx.cfg.apps[name] = modVal.(App)
|
|
|
|
return modVal, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Storage returns the configured Caddy storage implementation.
|
|
|
|
func (ctx Context) Storage() certmagic.Storage {
|
|
|
|
return ctx.cfg.storage
|
|
|
|
}
|
2019-10-29 04:39:37 +08:00
|
|
|
|
|
|
|
// Logger returns a logger that can be used by mod.
|
|
|
|
func (ctx Context) Logger(mod Module) *zap.Logger {
|
|
|
|
return ctx.cfg.Logging.Logger(mod)
|
|
|
|
}
|