/*
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
*
* 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.
*/
package event
import (
"encoding/xml"
"errors"
"io"
"reflect"
"strings"
"unicode/utf8"
"github.com/minio/minio-go/v6/pkg/set"
)
// ValidateFilterRuleValue - checks if given value is filter rule value or not.
func ValidateFilterRuleValue(value string) error {
for _, segment := range strings.Split(value, "/") {
if segment == "." || segment == ".." {
return &ErrInvalidFilterValue{value}
}
}
if len(value) <= 1024 && utf8.ValidString(value) && !strings.Contains(value, `\`) {
return nil
}
return &ErrInvalidFilterValue{value}
}
// FilterRule - represents elements inside ...
type FilterRule struct {
Name string `xml:"Name"`
Value string `xml:"Value"`
}
// UnmarshalXML - decodes XML data.
func (filter *FilterRule) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Make subtype to avoid recursive UnmarshalXML().
type filterRule FilterRule
rule := filterRule{}
if err := d.DecodeElement(&rule, &start); err != nil {
return err
}
if rule.Name != "prefix" && rule.Name != "suffix" {
return &ErrInvalidFilterName{rule.Name}
}
if err := ValidateFilterRuleValue(filter.Value); err != nil {
return err
}
*filter = FilterRule(rule)
return nil
}
// FilterRuleList - represents multiple ...
type FilterRuleList struct {
Rules []FilterRule `xml:"FilterRule,omitempty"`
}
// UnmarshalXML - decodes XML data.
func (ruleList *FilterRuleList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Make subtype to avoid recursive UnmarshalXML().
type filterRuleList FilterRuleList
rules := filterRuleList{}
if err := d.DecodeElement(&rules, &start); err != nil {
return err
}
// FilterRuleList must have only one prefix and/or suffix.
nameSet := set.NewStringSet()
for _, rule := range rules.Rules {
if nameSet.Contains(rule.Name) {
if rule.Name == "prefix" {
return &ErrFilterNamePrefix{}
}
return &ErrFilterNameSuffix{}
}
nameSet.Add(rule.Name)
}
*ruleList = FilterRuleList(rules)
return nil
}
// Pattern - returns pattern using prefix and suffix values.
func (ruleList FilterRuleList) Pattern() string {
var prefix string
var suffix string
for _, rule := range ruleList.Rules {
switch rule.Name {
case "prefix":
prefix = rule.Value
case "suffix":
suffix = rule.Value
}
}
return NewPattern(prefix, suffix)
}
// S3Key - represents elements inside ...
type S3Key struct {
RuleList FilterRuleList `xml:"S3Key,omitempty" json:"S3Key,omitempty"`
}
// common - represents common elements inside ,
// and
type common struct {
ID string `xml:"Id" json:"Id"`
Filter S3Key `xml:"Filter" json:"Filter"`
Events []Name `xml:"Event" json:"Event"`
}
// Queue - represents elements inside
type Queue struct {
common
ARN ARN `xml:"Queue"`
}
// UnmarshalXML - decodes XML data.
func (q *Queue) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Make subtype to avoid recursive UnmarshalXML().
type queue Queue
parsedQueue := queue{}
if err := d.DecodeElement(&parsedQueue, &start); err != nil {
return err
}
if len(parsedQueue.Events) == 0 {
return errors.New("missing event name(s)")
}
eventStringSet := set.NewStringSet()
for _, eventName := range parsedQueue.Events {
if eventStringSet.Contains(eventName.String()) {
return &ErrDuplicateEventName{eventName}
}
eventStringSet.Add(eventName.String())
}
*q = Queue(parsedQueue)
return nil
}
// Validate - checks whether queue has valid values or not.
func (q Queue) Validate(region string, targetList *TargetList) error {
if q.ARN.region == "" {
if !targetList.Exists(q.ARN.TargetID) {
return &ErrARNNotFound{q.ARN}
}
return nil
}
if region != "" && q.ARN.region != region {
return &ErrUnknownRegion{q.ARN.region}
}
if !targetList.Exists(q.ARN.TargetID) {
return &ErrARNNotFound{q.ARN}
}
return nil
}
// SetRegion - sets region value to queue's ARN.
func (q *Queue) SetRegion(region string) {
q.ARN.region = region
}
// ToRulesMap - converts Queue to RulesMap
func (q Queue) ToRulesMap() RulesMap {
pattern := q.Filter.RuleList.Pattern()
return NewRulesMap(q.Events, pattern, q.ARN.TargetID)
}
// Unused. Available for completion.
type lambda struct {
ARN string `xml:"CloudFunction"`
}
// Unused. Available for completion.
type topic struct {
ARN string `xml:"Topic" json:"Topic"`
}
// Config - notification configuration described in
// http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
type Config struct {
XMLNS string `xml:"xmlns,attr,omitempty"`
XMLName xml.Name `xml:"NotificationConfiguration"`
QueueList []Queue `xml:"QueueConfiguration,omitempty"`
LambdaList []lambda `xml:"CloudFunctionConfiguration,omitempty"`
TopicList []topic `xml:"TopicConfiguration,omitempty"`
}
// UnmarshalXML - decodes XML data.
func (conf *Config) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Make subtype to avoid recursive UnmarshalXML().
type config Config
parsedConfig := config{}
if err := d.DecodeElement(&parsedConfig, &start); err != nil {
return err
}
// Empty queue list means user wants to delete the notification configuration.
if len(parsedConfig.QueueList) > 0 {
for i, q1 := range parsedConfig.QueueList[:len(parsedConfig.QueueList)-1] {
for _, q2 := range parsedConfig.QueueList[i+1:] {
// Removes the region from ARN if server region is not set
if q2.ARN.region != "" && q1.ARN.region == "" {
q2.ARN.region = ""
}
if reflect.DeepEqual(q1, q2) {
return &ErrDuplicateQueueConfiguration{q1}
}
}
}
}
if len(parsedConfig.LambdaList) > 0 || len(parsedConfig.TopicList) > 0 {
return &ErrUnsupportedConfiguration{}
}
*conf = Config(parsedConfig)
return nil
}
// Validate - checks whether config has valid values or not.
func (conf Config) Validate(region string, targetList *TargetList) error {
for _, queue := range conf.QueueList {
if err := queue.Validate(region, targetList); err != nil {
return err
}
}
return nil
}
// SetRegion - sets region to all queue configuration.
func (conf *Config) SetRegion(region string) {
for i := range conf.QueueList {
conf.QueueList[i].SetRegion(region)
}
}
// ToRulesMap - converts all queue configuration to RulesMap.
func (conf *Config) ToRulesMap() RulesMap {
rulesMap := make(RulesMap)
for _, queue := range conf.QueueList {
rulesMap.Add(queue.ToRulesMap())
}
return rulesMap
}
// ParseConfig - parses data in reader to notification configuration.
func ParseConfig(reader io.Reader, region string, targetList *TargetList) (*Config, error) {
var config Config
if err := xml.NewDecoder(reader).Decode(&config); err != nil {
return nil, err
}
if err := config.Validate(region, targetList); err != nil {
return nil, err
}
config.SetRegion(region)
//If xml namespace is empty, set a default value before returning.
if config.XMLNS == "" {
config.XMLNS = "http://s3.amazonaws.com/doc/2006-03-01/"
}
return &config, nil
}