// Copyright Project Harbor 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. package utils import ( "crypto/rand" "encoding/json" "errors" "fmt" "net" "net/url" "reflect" "regexp" "strconv" "strings" "time" cronlib "github.com/robfig/cron/v3" "github.com/goharbor/harbor/src/lib/log" ) // ParseEndpoint parses endpoint to a URL func ParseEndpoint(endpoint string) (*url.URL, error) { endpoint = strings.Trim(endpoint, " ") endpoint = strings.TrimRight(endpoint, "/") if len(endpoint) == 0 { return nil, fmt.Errorf("empty URL") } i := strings.Index(endpoint, "://") if i >= 0 { scheme := endpoint[:i] if scheme != "http" && scheme != "https" { return nil, fmt.Errorf("invalid scheme: %s", scheme) } } else { endpoint = "http://" + endpoint } return url.ParseRequestURI(endpoint) } // ParseRepository splits a repository into two parts: project and rest func ParseRepository(repository string) (project, rest string) { repository = strings.TrimLeft(repository, "/") repository = strings.TrimRight(repository, "/") if !strings.ContainsRune(repository, '/') { rest = repository return } index := strings.Index(repository, "/") project = repository[0:index] rest = repository[index+1:] return } // GenerateRandomStringWithLen generates a random string with length func GenerateRandomStringWithLen(length int) string { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" l := len(chars) result := make([]byte, length) _, err := rand.Read(result) if err != nil { log.Warningf("Error reading random bytes: %v", err) } for i := 0; i < length; i++ { result[i] = chars[int(result[i])%l] } return string(result) } // GenerateRandomString generate a random string with 32 byte length func GenerateRandomString() string { return GenerateRandomStringWithLen(32) } // TestTCPConn tests TCP connection // timeout: the total time before returning if something is wrong // with the connection, in second // interval: the interval time for retring after failure, in second func TestTCPConn(addr string, timeout, interval int) error { success := make(chan int, 1) cancel := make(chan int, 1) go func() { n := 1 loop: for { select { case <-cancel: break loop default: conn, err := net.DialTimeout("tcp", addr, time.Duration(n)*time.Second) if err != nil { log.Errorf("failed to connect to tcp://%s, retry after %d seconds :%v", addr, interval, err) n = n * 2 time.Sleep(time.Duration(interval) * time.Second) continue } if err = conn.Close(); err != nil { log.Errorf("failed to close the connection: %v", err) } success <- 1 break loop } } }() select { case <-success: return nil case <-time.After(time.Duration(timeout) * time.Second): cancel <- 1 return fmt.Errorf("failed to connect to tcp:%s after %d seconds", addr, timeout) } } // ParseTimeStamp parse timestamp to time func ParseTimeStamp(timestamp string) (*time.Time, error) { i, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return nil, err } t := time.Unix(i, 0) return &t, nil } // ConvertMapToStruct is used to fill the specified struct with map. func ConvertMapToStruct(object interface{}, values interface{}) error { if object == nil { return errors.New("nil struct is not supported") } if reflect.TypeOf(object).Kind() != reflect.Ptr { return errors.New("object should be referred by pointer") } bytes, err := json.Marshal(values) if err != nil { return err } return json.Unmarshal(bytes, object) } // ParseProjectIDOrName parses value to ID(int64) or name(string) func ParseProjectIDOrName(value interface{}) (int64, string, error) { if value == nil { return 0, "", errors.New("harborIDOrName is nil") } var id int64 var name string switch v := value.(type) { case int, int64: id = reflect.ValueOf(v).Int() case string: name = value.(string) default: return 0, "", fmt.Errorf("unsupported type") } return id, name, nil } // SafeCastString -- cast a object to string saftely func SafeCastString(value interface{}) string { if result, ok := value.(string); ok { return result } return "" } // SafeCastInt -- func SafeCastInt(value interface{}) int { if result, ok := value.(int); ok { return result } return 0 } // SafeCastBool -- func SafeCastBool(value interface{}) bool { if result, ok := value.(bool); ok { return result } return false } // SafeCastFloat64 -- func SafeCastFloat64(value interface{}) float64 { if result, ok := value.(float64); ok { return result } return 0 } // TrimLower ... func TrimLower(str string) string { return strings.TrimSpace(strings.ToLower(str)) } // GetStrValueOfAnyType return string format of any value, for map, need to convert to json func GetStrValueOfAnyType(value interface{}) string { var strVal string if _, ok := value.(map[string]interface{}); ok { b, err := json.Marshal(value) if err != nil { log.Errorf("can not marshal json object, error %v", err) return "" } strVal = string(b) } else { switch val := value.(type) { case float64: strVal = strconv.FormatFloat(val, 'f', -1, 64) case float32: strVal = strconv.FormatFloat(float64(val), 'f', -1, 32) default: strVal = fmt.Sprintf("%v", value) } } return strVal } // IsIllegalLength ... func IsIllegalLength(s string, min int, max int) bool { if min == -1 { return (len(s) > max) } if max == -1 { return (len(s) <= min) } return (len(s) < min || len(s) > max) } // ParseJSONInt ... func ParseJSONInt(value interface{}) (int, bool) { switch v := value.(type) { case float64: return int(v), true case int: return v, true default: return 0, false } } // FindNamedMatches returns named matches of the regexp groups func FindNamedMatches(regex *regexp.Regexp, str string) map[string]string { match := regex.FindStringSubmatch(str) results := map[string]string{} for i, name := range match { results[regex.SubexpNames()[i]] = name } return results } // NextSchedule return next scheduled time with a cron string and current time provided // the cron string could contain 6 tokens // if the cron string is invalid, it returns a zero time func NextSchedule(cron string, curTime time.Time) time.Time { if len(cron) == 0 { return time.Time{} } cr := strings.TrimSpace(cron) s, err := CronParser().Parse(cr) if err != nil { log.Debugf("the cron string %v is invalid, error: %v", cron, err) return time.Time{} } return s.Next(curTime) } // CronParser returns the parser of cron string with format of "* * * * * *" func CronParser() cronlib.Parser { return cronlib.NewParser(cronlib.Second | cronlib.Minute | cronlib.Hour | cronlib.Dom | cronlib.Month | cronlib.Dow) } // ValidateCronString check whether it is a valid cron string and whether the 1st field (indicating Seconds of time) of the cron string is a fixed value of 0 or not func ValidateCronString(cron string) error { if len(cron) == 0 { return fmt.Errorf("empty cron string is invalid") } _, err := CronParser().Parse(cron) if err != nil { return err } cronParts := strings.Split(cron, " ") if len(cronParts) == 6 && cronParts[0] != "0" { return fmt.Errorf("the 1st field (indicating Seconds of time) of the cron setting must be 0") } return nil } // MostMatchSorter is a sorter for the most match, usually invoked in sort Less function // usage: // // sort.Slice(input, func(i, j int) bool { // return MostMatchSorter(input[i].GroupName, input[j].GroupName, matchWord) // }) // // a is the field to be used for sorting, b is the other field, matchWord is the word to be matched // the return value is true if a is less than b // for example, search with "user", input is {"harbor_user", "user", "users, "admin_user"} // it returns with this order {"user", "users", "admin_user", "harbor_user"} func MostMatchSorter(a, b string, matchWord string) bool { // exact match always first if a == matchWord { return true } if b == matchWord { return false } // sort by length, then sort by alphabet if len(a) == len(b) { return a < b } return len(a) < len(b) } // IsLocalPath checks if path is local, includes the empty path func IsLocalPath(path string) bool { return len(path) == 0 || (strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "//")) }