github.com/git-amp/amp-sdk-go@v0.7.5/stdlib/utils/http.go (about) 1 package utils 2 3 import ( 4 "crypto/tls" 5 "encoding" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "reflect" 11 "regexp" 12 "strings" 13 "time" 14 15 "github.com/rs/cors" 16 17 "github.com/git-amp/amp-sdk-go/stdlib/errors" 18 ) 19 20 type HTTPClient struct { 21 http.Client 22 chStop chan struct{} 23 } 24 25 func MakeHTTPClient(requestTimeout, reapIdleConnsInterval time.Duration, cookieJar http.CookieJar, tlsCerts []tls.Certificate) *HTTPClient { 26 c := http.Client{ 27 Timeout: requestTimeout, 28 Jar: cookieJar, 29 } 30 31 c.Transport = &http.Transport{ 32 TLSClientConfig: &tls.Config{ 33 MinVersion: tls.VersionTLS13, 34 MaxVersion: tls.VersionTLS13, 35 Certificates: tlsCerts, 36 ClientAuth: tls.RequestClientCert, 37 InsecureSkipVerify: true, 38 }, 39 } 40 41 chStop := make(chan struct{}) 42 43 if reapIdleConnsInterval > 0 { 44 go func() { 45 ticker := time.NewTicker(reapIdleConnsInterval) 46 defer ticker.Stop() 47 defer c.CloseIdleConnections() 48 49 for { 50 select { 51 case <-ticker.C: 52 c.CloseIdleConnections() 53 case <-chStop: 54 return 55 } 56 } 57 }() 58 } 59 60 return &HTTPClient{c, chStop} 61 } 62 63 func (c HTTPClient) Close() { 64 close(c.chStop) 65 } 66 67 var unmarshalRequestRegexp = regexp.MustCompile(`(header|query):"([^"]+)"`) 68 var stringType = reflect.TypeOf("") 69 70 func UnmarshalHTTPRequest(into interface{}, r *http.Request) error { 71 rval := reflect.ValueOf(into).Elem() 72 73 for i := 0; i < rval.Type().NumField(); i++ { 74 field := rval.Type().Field(i) 75 matches := unmarshalRequestRegexp.FindAllStringSubmatch(string(field.Tag), -1) 76 var found bool 77 for _, match := range matches { 78 source := match[1] 79 name := match[2] 80 81 var value string 82 switch source { 83 case "header": 84 value = r.Header.Get(name) 85 case "query": 86 value = r.URL.Query().Get(name) 87 default: 88 panic("invariant violation") 89 } 90 91 if value == "" { 92 continue 93 } 94 95 fieldVal := rval.Field(i) 96 97 var err error 98 if fieldVal.Kind() == reflect.Ptr { 99 err = unmarshalHTTPRequestField(field.Name, value, fieldVal) 100 } else if fieldVal.CanAddr() { 101 err = unmarshalHTTPRequestField(field.Name, value, fieldVal.Addr()) 102 } else { 103 return errors.Errorf("cannot unmarshal into struct field '%v'", field.Name) 104 } 105 if err != nil { 106 return err 107 } 108 found = true 109 break 110 } 111 if !found { 112 if field.Tag.Get("required") != "" { 113 return errors.Errorf("missing request field '%v'", field.Name) 114 } 115 } 116 } 117 return nil 118 } 119 120 func unmarshalHTTPRequestField(name, value string, fieldVal reflect.Value) error { 121 if as, is := fieldVal.Interface().(encoding.TextUnmarshaler); is { 122 return as.UnmarshalText([]byte(value)) 123 } else if fieldVal.Type().Elem().ConvertibleTo(stringType) { 124 fieldVal.Elem().Set(reflect.ValueOf(value).Convert(fieldVal.Type().Elem())) 125 return nil 126 } 127 panic(fmt.Sprintf(`cannot unmarshal http.Request into struct field "%v" of type %T`, name, fieldVal.Type())) 128 } 129 130 func RespondJSON(resp http.ResponseWriter, data interface{}) { 131 resp.Header().Add("Content-Type", "application/json") 132 133 err := json.NewEncoder(resp).Encode(data) 134 if err != nil { 135 panic(err) 136 } 137 } 138 139 func UnrestrictedCors(handler http.Handler) http.Handler { 140 return cors.New(cors.Options{ 141 AllowOriginFunc: func(string) bool { return true }, 142 AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "AUTHORIZE", "SUBSCRIBE", "ACK", "OPTIONS", "HEAD"}, 143 AllowedHeaders: []string{"*"}, 144 ExposedHeaders: []string{"*"}, 145 AllowCredentials: true, 146 }).Handler(handler) 147 } 148 149 func SniffContentType(filename string, data io.Reader) (string, error) { 150 // Only the first 512 bytes are used to sniff the content type. 151 buffer := make([]byte, 512) 152 153 _, err := data.Read(buffer) 154 if err != nil { 155 return "", err 156 } 157 158 // Use the net/http package's handy DectectContentType function. Always returns a valid 159 // content-type by returning "application/octet-stream" if no others seemed to match. 160 contentType := http.DetectContentType(buffer) 161 162 // If we got an ambiguous result, check the file extension 163 if contentType == "application/octet-stream" { 164 contentType = GuessContentTypeFromFilename(filename) 165 } 166 return contentType, nil 167 } 168 169 func GuessContentTypeFromFilename(filename string) string { 170 parts := strings.Split(filename, ".") 171 if len(parts) > 1 { 172 ext := strings.ToLower(parts[len(parts)-1]) 173 switch ext { 174 case "txt": 175 return "text/plain" 176 case "html": 177 return "text/html" 178 case "js": 179 return "application/js" 180 case "json": 181 return "application/json" 182 case "png": 183 return "image/png" 184 case "jpg", "jpeg": 185 return "image/jpeg" 186 } 187 } 188 return "application/octet-stream" 189 }