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  }