264 lines
5.7 KiB
Go
264 lines
5.7 KiB
Go
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.unistack.org/micro/v4/client"
|
|
"go.unistack.org/micro/v4/codec"
|
|
"go.unistack.org/micro/v4/logger"
|
|
"go.unistack.org/micro/v4/metadata"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/reflect/protoreflect"
|
|
|
|
"go.unistack.org/micro-client-http/v4/builder"
|
|
)
|
|
|
|
func buildHTTPRequest(
|
|
ctx context.Context,
|
|
addr string,
|
|
path string,
|
|
ct string,
|
|
cf codec.Codec,
|
|
msg any,
|
|
opts client.CallOptions,
|
|
log logger.Logger,
|
|
) (
|
|
*http.Request,
|
|
error,
|
|
) {
|
|
protoMsg, ok := msg.(proto.Message)
|
|
if !ok {
|
|
return nil, errors.New("msg must be a proto message type")
|
|
}
|
|
|
|
var (
|
|
method = http.MethodPost
|
|
bodyOpt string
|
|
|
|
parameters = map[string]map[string]string{}
|
|
)
|
|
|
|
if opts.Context != nil {
|
|
if v, ok := methodFromOpts(opts); ok {
|
|
method = v
|
|
}
|
|
if v, ok := pathFromOpts(opts); ok {
|
|
path = v
|
|
}
|
|
if v, ok := bodyFromOpts(opts); ok {
|
|
bodyOpt = v
|
|
}
|
|
if h, ok := headerFromOpts(opts); ok && len(h) > 0 {
|
|
m, ok := parameters["header"]
|
|
if !ok {
|
|
m = make(map[string]string)
|
|
parameters["header"] = m
|
|
}
|
|
for idx := 0; idx+1 < len(h); idx += 2 {
|
|
m[h[idx]] = h[idx+1]
|
|
}
|
|
}
|
|
if c, ok := cookieFromOpts(opts); ok && len(c) > 0 {
|
|
m, ok := parameters["cookie"]
|
|
if !ok {
|
|
m = make(map[string]string)
|
|
parameters["cookie"] = m
|
|
}
|
|
for idx := 0; idx+1 < len(c); idx += 2 {
|
|
m[c[idx]] = c[idx+1]
|
|
}
|
|
}
|
|
}
|
|
|
|
reqBuilder, err := builder.NewRequestBuilder(path, method, bodyOpt, protoMsg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new request builder: %w", err)
|
|
}
|
|
|
|
resolvedPath, newMsg, err := reqBuilder.Build()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build request: %w", err)
|
|
}
|
|
|
|
resolvedURL := joinURL(addr, resolvedPath)
|
|
|
|
u, err := normalizeURL(resolvedURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("normalize url: %w", err)
|
|
}
|
|
|
|
body, err := marshallMsg(cf, newMsg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal msg: %w", err)
|
|
}
|
|
|
|
var hreq *http.Request
|
|
|
|
if len(body) > 0 {
|
|
hreq, err = http.NewRequestWithContext(ctx, method, u.String(), io.NopCloser(bytes.NewBuffer(body)))
|
|
hreq.ContentLength = int64(len(body))
|
|
} else {
|
|
hreq, err = http.NewRequestWithContext(ctx, method, u.String(), nil)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new http request: %w", err)
|
|
}
|
|
|
|
setHeadersAndCookies(ctx, hreq, ct, opts)
|
|
if err = validateHeadersAndCookies(hreq, parameters); err != nil {
|
|
return nil, fmt.Errorf("validate headers and cookies: %w", err)
|
|
}
|
|
|
|
if log.V(logger.DebugLevel) {
|
|
log.Debug(
|
|
ctx,
|
|
fmt.Sprintf("request %s to %s with headers %v body %s", method, u.String(), hreq.Header, body),
|
|
)
|
|
}
|
|
|
|
return hreq, nil
|
|
}
|
|
|
|
func joinURL(addr, resolvedPath string) string {
|
|
if addr == "" {
|
|
return resolvedPath
|
|
}
|
|
if resolvedPath == "" {
|
|
return addr
|
|
}
|
|
|
|
switch {
|
|
case strings.HasSuffix(addr, "/") && strings.HasPrefix(resolvedPath, "/"):
|
|
return addr + resolvedPath[1:]
|
|
case !strings.HasSuffix(addr, "/") && !strings.HasPrefix(resolvedPath, "/"):
|
|
return addr + "/" + resolvedPath
|
|
default:
|
|
return addr + resolvedPath
|
|
}
|
|
}
|
|
|
|
func normalizeURL(raw string) (*url.URL, error) {
|
|
if !strings.Contains(raw, "://") {
|
|
raw = "http://" + raw
|
|
}
|
|
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse url: %w", err)
|
|
}
|
|
|
|
if u.Scheme != "http" && u.Scheme != "https" {
|
|
return nil, fmt.Errorf("invalid scheme: %q (must be http or https)", u.Scheme)
|
|
}
|
|
|
|
if u.Host == "" {
|
|
return nil, errors.New("missing host in url")
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
func marshallMsg(cf codec.Codec, msg proto.Message) ([]byte, error) {
|
|
if msg == nil {
|
|
return nil, nil
|
|
}
|
|
isEmpty := true
|
|
msg.ProtoReflect().Range(func(protoreflect.FieldDescriptor, protoreflect.Value) bool {
|
|
isEmpty = false
|
|
return false
|
|
})
|
|
if isEmpty {
|
|
return nil, nil
|
|
}
|
|
return cf.Marshal(msg)
|
|
}
|
|
|
|
func setHeadersAndCookies(ctx context.Context, r *http.Request, ct string, opts client.CallOptions) {
|
|
r.Header = make(http.Header)
|
|
|
|
r.Header.Set(metadata.HeaderContentType, ct)
|
|
r.Header.Set("Content-Length", fmt.Sprintf("%d", r.ContentLength))
|
|
|
|
if opts.AuthToken != "" {
|
|
r.Header.Set(metadata.HeaderAuthorization, opts.AuthToken)
|
|
}
|
|
|
|
if opts.StreamTimeout > time.Duration(0) {
|
|
r.Header.Set(metadata.HeaderTimeout, fmt.Sprintf("%d", opts.StreamTimeout))
|
|
}
|
|
if opts.RequestTimeout > time.Duration(0) {
|
|
r.Header.Set(metadata.HeaderTimeout, fmt.Sprintf("%d", opts.RequestTimeout))
|
|
}
|
|
|
|
if opts.RequestMetadata != nil {
|
|
for k, v := range opts.RequestMetadata {
|
|
if k == "Cookie" {
|
|
applyCookies(r, v)
|
|
continue
|
|
}
|
|
r.Header[k] = append(r.Header[k], v...)
|
|
}
|
|
}
|
|
|
|
if md, ok := metadata.FromOutgoingContext(ctx); ok {
|
|
for k, v := range md {
|
|
if k == "Cookie" {
|
|
applyCookies(r, v)
|
|
continue
|
|
}
|
|
r.Header[k] = append(r.Header[k], v...)
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyCookies(r *http.Request, rawCookies []string) {
|
|
if len(rawCookies) == 0 {
|
|
return
|
|
}
|
|
|
|
raw := strings.Join(rawCookies, "; ")
|
|
|
|
tmp := http.Request{Header: http.Header{}}
|
|
tmp.Header.Set("Cookie", raw)
|
|
|
|
for _, c := range tmp.Cookies() {
|
|
r.AddCookie(c)
|
|
}
|
|
}
|
|
|
|
func validateHeadersAndCookies(r *http.Request, parameters map[string]map[string]string) error {
|
|
if headers, ok := parameters["header"]; ok {
|
|
for name, required := range headers {
|
|
if required == "true" && r.Header.Get(name) == "" {
|
|
return fmt.Errorf("missing required header: %s", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
if cookies, ok := parameters["cookie"]; ok {
|
|
cookieMap := map[string]string{}
|
|
for _, c := range r.Cookies() {
|
|
cookieMap[c.Name] = c.Value
|
|
}
|
|
|
|
for name, required := range cookies {
|
|
if required == "true" {
|
|
if _, ok := cookieMap[name]; !ok {
|
|
return fmt.Errorf("missing required cookie: %s", name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|