-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathproxy.go
161 lines (137 loc) · 4.4 KB
/
proxy.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/*
Proxy requests to api.yr.no so that they can be accessed from web pages.
To deploy, upload the contents of this file to Google Cloud Run:
https://console.cloud.google.com/run/detail/europe-north1/api-met-no-proxy-go/source?inv=1&invt=Abq9XQ&project=api-met-no-proxy
*/
package proxy
import (
"fmt"
"io"
"log/slog"
"net/http"
"os"
"strings"
"time"
)
func headersToValue(header http.Header) slog.Value {
var headers []slog.Attr
for key, values := range header {
headers = append(headers, slog.String(key, strings.Join(values, ",")))
}
return slog.GroupValue(headers...)
}
var log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Translate from Go to GCP log field names
// https://go.dev/blog/slog
// https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields
if a.Key == "level" {
a.Key = "severity"
}
if a.Key == "msg" {
a.Key = "message"
}
// Special handle http.Request type fields
if r, ok := a.Value.Any().(*http.Request); ok {
// Field names from here:
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest
a.Value = slog.GroupValue(
slog.String("requestMethod", r.Method),
slog.String("requestUrl", r.URL.String()),
slog.String("protocol", r.Proto),
slog.String("remoteIp", r.RemoteAddr),
slog.Attr{
Key: "headers",
Value: headersToValue(r.Header),
},
)
}
if r, ok := a.Value.Any().(*http.Response); ok {
a.Value = slog.GroupValue(
slog.String("requestMethod", r.Request.Method),
slog.String("requestUrl", r.Request.URL.String()),
slog.String("protocol", r.Proto),
slog.String("status", r.Status),
slog.Int64("responseSize", r.ContentLength),
slog.Attr{
Key: "headers",
Value: headersToValue(r.Header),
},
)
}
return a
},
}))
var httpClient = &http.Client{
Timeout: 10 * time.Second,
}
func ProxyRequest(w http.ResponseWriter, r *http.Request) {
proxyRequest("https://api.met.no/weatherapi", w, r)
}
// Remove hop-by-hop headers:
// https://www.freesoft.org/CIE/RFC/2068/143.htm
func dropHopByHopHeaders(header http.Header) {
header.Del("Connection")
header.Del("Keep-Alive")
header.Del("Proxy-Authenticate")
header.Del("Proxy-Authorization")
header.Del("TE")
header.Del("Trailer")
header.Del("Transfer-Encoding")
header.Del("Upgrade")
}
func proxyRequest(yrNoAPIBaseURL string, w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
log.Error("Method must be GET", "httpRequest", r)
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Create a new request to the API
yrURL := yrNoAPIBaseURL + r.URL.Path
yrRequest, err := http.NewRequest(r.Method, yrURL, r.Body)
if err != nil {
log.Error("Failed to create upstream request from incoming", "error", err, "httpRequest", r)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Copy the query parameters from the original request
yrRequest.URL.RawQuery = r.URL.RawQuery
// Copy the headers from the original request
yrRequest.Header = r.Header.Clone()
dropHopByHopHeaders(yrRequest.Header)
t0 := time.Now()
// Send the request to the API
yrResponse, err := httpClient.Do(yrRequest)
if err != nil {
log.Error("Failed to send outgoing request", "error", err, "httpRequest", r, "outgoing", yrRequest)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer yrResponse.Body.Close()
// Copy the response headers
for key, values := range yrResponse.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
dropHopByHopHeaders(w.Header())
// Allow using from JavaScript
w.Header().Set("Access-Control-Allow-Origin", "*")
// Copy the response status code
w.WriteHeader(yrResponse.StatusCode)
// Tell the client to cache forecasts for two hours, they are unlikely to
// change much.
w.Header().Set("Cache-Control", "public, max-age=7200")
// Copy the response body
_, err = io.Copy(w, yrResponse.Body)
if err != nil {
log.Error("Failed to copy response body", "error", err, "httpRequest", r, "yrResponse", yrResponse)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dt := time.Since(t0)
log.Info(fmt.Sprintf(
"Got %d bytes of weather data in %s from %s", yrResponse.ContentLength, dt.String(), yrURL+"?"+r.URL.RawQuery),
"httpRequest", r,
"yrResponse", yrResponse)
}