init
This commit is contained in:
+202
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
|
||||
+581
@@ -0,0 +1,581 @@
|
||||
// Package forward implements http handler that forwards requests to remote server
|
||||
// and serves back the response
|
||||
// websocket proxying support based on https://github.com/yhat/wsutil
|
||||
package forward
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vulcand/oxy/utils"
|
||||
)
|
||||
|
||||
// OxyLogger interface of the internal
|
||||
type OxyLogger interface {
|
||||
log.FieldLogger
|
||||
GetLevel() log.Level
|
||||
}
|
||||
|
||||
type internalLogger struct {
|
||||
*log.Logger
|
||||
}
|
||||
|
||||
func (i *internalLogger) GetLevel() log.Level {
|
||||
return i.Level
|
||||
}
|
||||
|
||||
// ReqRewriter can alter request headers and body
|
||||
type ReqRewriter interface {
|
||||
Rewrite(r *http.Request)
|
||||
}
|
||||
|
||||
type optSetter func(f *Forwarder) error
|
||||
|
||||
// PassHostHeader specifies if a client's Host header field should be delegated
|
||||
func PassHostHeader(b bool) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
f.httpForwarder.passHost = b
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// RoundTripper sets a new http.RoundTripper
|
||||
// Forwarder will use http.DefaultTransport as a default round tripper
|
||||
func RoundTripper(r http.RoundTripper) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
f.httpForwarder.roundTripper = r
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Rewriter defines a request rewriter for the HTTP forwarder
|
||||
func Rewriter(r ReqRewriter) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
f.httpForwarder.rewriter = r
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketTLSClientConfig define the websocker client TLS configuration
|
||||
func WebsocketTLSClientConfig(tcc *tls.Config) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
f.httpForwarder.tlsClientConfig = tcc
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorHandler is a functional argument that sets error handler of the server
|
||||
func ErrorHandler(h utils.ErrorHandler) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
f.errHandler = h
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// BufferPool specifies a buffer pool for httputil.ReverseProxy.
|
||||
func BufferPool(pool httputil.BufferPool) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
f.bufferPool = pool
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Stream specifies if HTTP responses should be streamed.
|
||||
func Stream(stream bool) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
f.stream = stream
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Logger defines the logger the forwarder will use.
|
||||
//
|
||||
// It defaults to logrus.StandardLogger(), the global logger used by logrus.
|
||||
func Logger(l log.FieldLogger) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
if logger, ok := l.(OxyLogger); ok {
|
||||
f.log = logger
|
||||
return nil
|
||||
}
|
||||
|
||||
if logger, ok := l.(*log.Logger); ok {
|
||||
f.log = &internalLogger{Logger: logger}
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("the type of the logger must be OxyLogger or logrus.Logger")
|
||||
}
|
||||
}
|
||||
|
||||
// StateListener defines a state listener for the HTTP forwarder
|
||||
func StateListener(stateListener UrlForwardingStateListener) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
f.stateListener = stateListener
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketConnectionClosedHook defines a hook called when websocket connection is closed
|
||||
func WebsocketConnectionClosedHook(hook func(req *http.Request, conn net.Conn)) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
f.httpForwarder.websocketConnectionClosedHook = hook
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ResponseModifier defines a response modifier for the HTTP forwarder
|
||||
func ResponseModifier(responseModifier func(*http.Response) error) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
f.httpForwarder.modifyResponse = responseModifier
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// StreamingFlushInterval defines a streaming flush interval for the HTTP forwarder
|
||||
func StreamingFlushInterval(flushInterval time.Duration) optSetter {
|
||||
return func(f *Forwarder) error {
|
||||
f.httpForwarder.flushInterval = flushInterval
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorHandlingRoundTripper a error handling round tripper
|
||||
type ErrorHandlingRoundTripper struct {
|
||||
http.RoundTripper
|
||||
errorHandler utils.ErrorHandler
|
||||
}
|
||||
|
||||
// RoundTrip executes the round trip
|
||||
func (rt ErrorHandlingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
res, err := rt.RoundTripper.RoundTrip(req)
|
||||
if err != nil {
|
||||
// We use the recorder from httptest because there isn't another `public` implementation of a recorder.
|
||||
recorder := httptest.NewRecorder()
|
||||
rt.errorHandler.ServeHTTP(recorder, req, err)
|
||||
res = recorder.Result()
|
||||
err = nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Forwarder wraps two traffic forwarding implementations: HTTP and websockets.
|
||||
// It decides based on the specified request which implementation to use
|
||||
type Forwarder struct {
|
||||
*httpForwarder
|
||||
*handlerContext
|
||||
stateListener UrlForwardingStateListener
|
||||
stream bool
|
||||
}
|
||||
|
||||
// handlerContext defines a handler context for error reporting and logging
|
||||
type handlerContext struct {
|
||||
errHandler utils.ErrorHandler
|
||||
}
|
||||
|
||||
// httpForwarder is a handler that can reverse proxy
|
||||
// HTTP traffic
|
||||
type httpForwarder struct {
|
||||
roundTripper http.RoundTripper
|
||||
rewriter ReqRewriter
|
||||
passHost bool
|
||||
flushInterval time.Duration
|
||||
modifyResponse func(*http.Response) error
|
||||
|
||||
tlsClientConfig *tls.Config
|
||||
|
||||
log OxyLogger
|
||||
|
||||
bufferPool httputil.BufferPool
|
||||
websocketConnectionClosedHook func(req *http.Request, conn net.Conn)
|
||||
}
|
||||
|
||||
const defaultFlushInterval = time.Duration(100) * time.Millisecond
|
||||
|
||||
// Connection states
|
||||
const (
|
||||
StateConnected = iota
|
||||
StateDisconnected
|
||||
)
|
||||
|
||||
// UrlForwardingStateListener URL forwarding state listener
|
||||
type UrlForwardingStateListener func(*url.URL, int)
|
||||
|
||||
// New creates an instance of Forwarder based on the provided list of configuration options
|
||||
func New(setters ...optSetter) (*Forwarder, error) {
|
||||
f := &Forwarder{
|
||||
httpForwarder: &httpForwarder{log: &internalLogger{Logger: log.StandardLogger()}},
|
||||
handlerContext: &handlerContext{},
|
||||
}
|
||||
for _, s := range setters {
|
||||
if err := s(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !f.stream {
|
||||
f.flushInterval = 0
|
||||
} else if f.flushInterval == 0 {
|
||||
f.flushInterval = defaultFlushInterval
|
||||
}
|
||||
|
||||
if f.httpForwarder.rewriter == nil {
|
||||
h, err := os.Hostname()
|
||||
if err != nil {
|
||||
h = "localhost"
|
||||
}
|
||||
f.httpForwarder.rewriter = &HeaderRewriter{TrustForwardHeader: true, Hostname: h}
|
||||
}
|
||||
|
||||
if f.httpForwarder.roundTripper == nil {
|
||||
f.httpForwarder.roundTripper = http.DefaultTransport
|
||||
}
|
||||
|
||||
if f.errHandler == nil {
|
||||
f.errHandler = utils.DefaultHandler
|
||||
}
|
||||
|
||||
if f.tlsClientConfig == nil {
|
||||
if ht, ok := f.httpForwarder.roundTripper.(*http.Transport); ok {
|
||||
f.tlsClientConfig = ht.TLSClientConfig
|
||||
}
|
||||
}
|
||||
|
||||
f.httpForwarder.roundTripper = ErrorHandlingRoundTripper{
|
||||
RoundTripper: f.httpForwarder.roundTripper,
|
||||
errorHandler: f.errHandler,
|
||||
}
|
||||
|
||||
f.postConfig()
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ServeHTTP decides which forwarder to use based on the specified
|
||||
// request and delegates to the proper implementation
|
||||
func (f *Forwarder) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
if f.log.GetLevel() >= log.DebugLevel {
|
||||
logEntry := f.log.WithField("Request", utils.DumpHttpRequest(req))
|
||||
logEntry.Debug("vulcand/oxy/forward: begin ServeHttp on request")
|
||||
defer logEntry.Debug("vulcand/oxy/forward: completed ServeHttp on request")
|
||||
}
|
||||
|
||||
if f.stateListener != nil {
|
||||
f.stateListener(req.URL, StateConnected)
|
||||
defer f.stateListener(req.URL, StateDisconnected)
|
||||
}
|
||||
if IsWebsocketRequest(req) {
|
||||
f.httpForwarder.serveWebSocket(w, req, f.handlerContext)
|
||||
} else {
|
||||
f.httpForwarder.serveHTTP(w, req, f.handlerContext)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *httpForwarder) getUrlFromRequest(req *http.Request) *url.URL {
|
||||
// If the Request was created by Go via a real HTTP request, RequestURI will
|
||||
// contain the original query string. If the Request was created in code, RequestURI
|
||||
// will be empty, and we will use the URL object instead
|
||||
u := req.URL
|
||||
if req.RequestURI != "" {
|
||||
parsedURL, err := url.ParseRequestURI(req.RequestURI)
|
||||
if err == nil {
|
||||
u = parsedURL
|
||||
} else {
|
||||
f.log.Warnf("vulcand/oxy/forward: error when parsing RequestURI: %s", err)
|
||||
}
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// Modify the request to handle the target URL
|
||||
func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) {
|
||||
outReq.URL = utils.CopyURL(outReq.URL)
|
||||
outReq.URL.Scheme = target.Scheme
|
||||
outReq.URL.Host = target.Host
|
||||
|
||||
u := f.getUrlFromRequest(outReq)
|
||||
|
||||
outReq.URL.Path = u.Path
|
||||
outReq.URL.RawPath = u.RawPath
|
||||
outReq.URL.RawQuery = u.RawQuery
|
||||
outReq.RequestURI = "" // Outgoing request should not have RequestURI
|
||||
|
||||
outReq.Proto = "HTTP/1.1"
|
||||
outReq.ProtoMajor = 1
|
||||
outReq.ProtoMinor = 1
|
||||
|
||||
if f.rewriter != nil {
|
||||
f.rewriter.Rewrite(outReq)
|
||||
}
|
||||
|
||||
// Do not pass client Host header unless optsetter PassHostHeader is set.
|
||||
if !f.passHost {
|
||||
outReq.Host = target.Host
|
||||
}
|
||||
}
|
||||
|
||||
// serveHTTP forwards websocket traffic
|
||||
func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, ctx *handlerContext) {
|
||||
if f.log.GetLevel() >= log.DebugLevel {
|
||||
logEntry := f.log.WithField("Request", utils.DumpHttpRequest(req))
|
||||
logEntry.Debug("vulcand/oxy/forward/websocket: begin ServeHttp on request")
|
||||
defer logEntry.Debug("vulcand/oxy/forward/websocket: completed ServeHttp on request")
|
||||
}
|
||||
|
||||
outReq := f.copyWebSocketRequest(req)
|
||||
|
||||
dialer := websocket.DefaultDialer
|
||||
|
||||
if outReq.URL.Scheme == "wss" && f.tlsClientConfig != nil {
|
||||
dialer.TLSClientConfig = f.tlsClientConfig.Clone()
|
||||
// WebSocket is only in http/1.1
|
||||
dialer.TLSClientConfig.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
targetConn, resp, err := dialer.DialContext(outReq.Context(), outReq.URL.String(), outReq.Header)
|
||||
if err != nil {
|
||||
if resp == nil {
|
||||
ctx.errHandler.ServeHTTP(w, req, err)
|
||||
} else {
|
||||
f.log.Errorf("vulcand/oxy/forward/websocket: Error dialing %q: %v with resp: %d %s", outReq.Host, err, resp.StatusCode, resp.Status)
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
f.log.Errorf("vulcand/oxy/forward/websocket: %s can not be hijack", reflect.TypeOf(w))
|
||||
ctx.errHandler.ServeHTTP(w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
conn, _, errHijack := hijacker.Hijack()
|
||||
if errHijack != nil {
|
||||
f.log.Errorf("vulcand/oxy/forward/websocket: Failed to hijack responseWriter")
|
||||
ctx.errHandler.ServeHTTP(w, req, errHijack)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
conn.Close()
|
||||
if f.websocketConnectionClosedHook != nil {
|
||||
f.websocketConnectionClosedHook(req, conn)
|
||||
}
|
||||
}()
|
||||
|
||||
errWrite := resp.Write(conn)
|
||||
if errWrite != nil {
|
||||
f.log.Errorf("vulcand/oxy/forward/websocket: Failed to forward response")
|
||||
ctx.errHandler.ServeHTTP(w, req, errWrite)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Only the targetConn choose to CheckOrigin or not
|
||||
upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
}}
|
||||
|
||||
utils.RemoveHeaders(resp.Header, WebsocketUpgradeHeaders...)
|
||||
utils.CopyHeaders(resp.Header, w.Header())
|
||||
|
||||
underlyingConn, err := upgrader.Upgrade(w, req, resp.Header)
|
||||
if err != nil {
|
||||
f.log.Errorf("vulcand/oxy/forward/websocket: Error while upgrading connection : %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
underlyingConn.Close()
|
||||
targetConn.Close()
|
||||
if f.websocketConnectionClosedHook != nil {
|
||||
f.websocketConnectionClosedHook(req, underlyingConn.UnderlyingConn())
|
||||
}
|
||||
}()
|
||||
|
||||
errClient := make(chan error, 1)
|
||||
errBackend := make(chan error, 1)
|
||||
replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) {
|
||||
|
||||
forward := func(messageType int, reader io.Reader) error {
|
||||
writer, err := dst.NextWriter(messageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(writer, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writer.Close()
|
||||
}
|
||||
|
||||
src.SetPingHandler(func(data string) error {
|
||||
return forward(websocket.PingMessage, bytes.NewReader([]byte(data)))
|
||||
})
|
||||
|
||||
src.SetPongHandler(func(data string) error {
|
||||
return forward(websocket.PongMessage, bytes.NewReader([]byte(data)))
|
||||
})
|
||||
|
||||
for {
|
||||
msgType, reader, err := src.NextReader()
|
||||
|
||||
if err != nil {
|
||||
m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err))
|
||||
if e, ok := err.(*websocket.CloseError); ok {
|
||||
if e.Code != websocket.CloseNoStatusReceived {
|
||||
m = nil
|
||||
// Following codes are not valid on the wire so just close the
|
||||
// underlying TCP connection without sending a close frame.
|
||||
if e.Code != websocket.CloseAbnormalClosure &&
|
||||
e.Code != websocket.CloseTLSHandshake {
|
||||
|
||||
m = websocket.FormatCloseMessage(e.Code, e.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
errc <- err
|
||||
if m != nil {
|
||||
forward(websocket.CloseMessage, bytes.NewReader([]byte(m)))
|
||||
}
|
||||
break
|
||||
}
|
||||
err = forward(msgType, reader)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go replicateWebsocketConn(underlyingConn, targetConn, errClient)
|
||||
go replicateWebsocketConn(targetConn, underlyingConn, errBackend)
|
||||
|
||||
var message string
|
||||
select {
|
||||
case err = <-errClient:
|
||||
message = "vulcand/oxy/forward/websocket: Error when copying from backend to client: %v"
|
||||
case err = <-errBackend:
|
||||
message = "vulcand/oxy/forward/websocket: Error when copying from client to backend: %v"
|
||||
|
||||
}
|
||||
if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure {
|
||||
f.log.Errorf(message, err)
|
||||
}
|
||||
}
|
||||
|
||||
// copyWebsocketRequest makes a copy of the specified request.
|
||||
func (f *httpForwarder) copyWebSocketRequest(req *http.Request) (outReq *http.Request) {
|
||||
outReq = new(http.Request)
|
||||
*outReq = *req // includes shallow copies of maps, but we handle this below
|
||||
|
||||
outReq.URL = utils.CopyURL(req.URL)
|
||||
outReq.URL.Scheme = req.URL.Scheme
|
||||
|
||||
// sometimes backends might be registered as HTTP/HTTPS servers so translate URLs to websocket URLs.
|
||||
switch req.URL.Scheme {
|
||||
case "https":
|
||||
outReq.URL.Scheme = "wss"
|
||||
case "http":
|
||||
outReq.URL.Scheme = "ws"
|
||||
}
|
||||
|
||||
u := f.getUrlFromRequest(outReq)
|
||||
|
||||
outReq.URL.Path = u.Path
|
||||
outReq.URL.RawPath = u.RawPath
|
||||
outReq.URL.RawQuery = u.RawQuery
|
||||
outReq.RequestURI = "" // Outgoing request should not have RequestURI
|
||||
|
||||
outReq.URL.Host = req.URL.Host
|
||||
if !f.passHost {
|
||||
outReq.Host = req.URL.Host
|
||||
}
|
||||
|
||||
outReq.Header = make(http.Header)
|
||||
// gorilla websocket use this header to set the request.Host tested in checkSameOrigin
|
||||
outReq.Header.Set("Host", outReq.Host)
|
||||
utils.CopyHeaders(outReq.Header, req.Header)
|
||||
utils.RemoveHeaders(outReq.Header, WebsocketDialHeaders...)
|
||||
|
||||
if f.rewriter != nil {
|
||||
f.rewriter.Rewrite(outReq)
|
||||
}
|
||||
return outReq
|
||||
}
|
||||
|
||||
// serveHTTP forwards HTTP traffic using the configured transport
|
||||
func (f *httpForwarder) serveHTTP(w http.ResponseWriter, inReq *http.Request, ctx *handlerContext) {
|
||||
if f.log.GetLevel() >= log.DebugLevel {
|
||||
logEntry := f.log.WithField("Request", utils.DumpHttpRequest(inReq))
|
||||
logEntry.Debug("vulcand/oxy/forward/http: begin ServeHttp on request")
|
||||
defer logEntry.Debug("vulcand/oxy/forward/http: completed ServeHttp on request")
|
||||
}
|
||||
|
||||
start := time.Now().UTC()
|
||||
|
||||
outReq := new(http.Request)
|
||||
*outReq = *inReq // includes shallow copies of maps, but we handle this in Director
|
||||
|
||||
revproxy := httputil.ReverseProxy{
|
||||
Director: func(req *http.Request) {
|
||||
f.modifyRequest(req, inReq.URL)
|
||||
},
|
||||
Transport: f.roundTripper,
|
||||
FlushInterval: f.flushInterval,
|
||||
ModifyResponse: f.modifyResponse,
|
||||
BufferPool: f.bufferPool,
|
||||
}
|
||||
|
||||
if f.log.GetLevel() >= log.DebugLevel {
|
||||
pw := utils.NewProxyWriter(w)
|
||||
revproxy.ServeHTTP(pw, outReq)
|
||||
|
||||
if inReq.TLS != nil {
|
||||
f.log.Debugf("vulcand/oxy/forward/http: Round trip: %v, code: %v, Length: %v, duration: %v tls:version: %x, tls:resume:%t, tls:csuite:%x, tls:server:%v",
|
||||
inReq.URL, pw.StatusCode(), pw.GetLength(), time.Now().UTC().Sub(start),
|
||||
inReq.TLS.Version,
|
||||
inReq.TLS.DidResume,
|
||||
inReq.TLS.CipherSuite,
|
||||
inReq.TLS.ServerName)
|
||||
} else {
|
||||
f.log.Debugf("vulcand/oxy/forward/http: Round trip: %v, code: %v, Length: %v, duration: %v",
|
||||
inReq.URL, pw.StatusCode(), pw.GetLength(), time.Now().UTC().Sub(start))
|
||||
}
|
||||
} else {
|
||||
revproxy.ServeHTTP(w, outReq)
|
||||
}
|
||||
|
||||
for key := range w.Header() {
|
||||
if strings.HasPrefix(key, http.TrailerPrefix) {
|
||||
if fl, ok := w.(http.Flusher); ok {
|
||||
fl.Flush()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// IsWebsocketRequest determines if the specified HTTP request is a
|
||||
// websocket handshake request
|
||||
func IsWebsocketRequest(req *http.Request) bool {
|
||||
containsHeader := func(name, value string) bool {
|
||||
items := strings.Split(req.Header.Get(name), ",")
|
||||
for _, item := range items {
|
||||
if value == strings.ToLower(strings.TrimSpace(item)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return containsHeader(Connection, "upgrade") && containsHeader(Upgrade, "websocket")
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package forward
|
||||
|
||||
// Headers
|
||||
const (
|
||||
XForwardedProto = "X-Forwarded-Proto"
|
||||
XForwardedFor = "X-Forwarded-For"
|
||||
XForwardedHost = "X-Forwarded-Host"
|
||||
XForwardedPort = "X-Forwarded-Port"
|
||||
XForwardedServer = "X-Forwarded-Server"
|
||||
XRealIp = "X-Real-Ip"
|
||||
Connection = "Connection"
|
||||
KeepAlive = "Keep-Alive"
|
||||
ProxyAuthenticate = "Proxy-Authenticate"
|
||||
ProxyAuthorization = "Proxy-Authorization"
|
||||
Te = "Te" // canonicalized version of "TE"
|
||||
Trailers = "Trailers"
|
||||
TransferEncoding = "Transfer-Encoding"
|
||||
Upgrade = "Upgrade"
|
||||
ContentLength = "Content-Length"
|
||||
SecWebsocketKey = "Sec-Websocket-Key"
|
||||
SecWebsocketVersion = "Sec-Websocket-Version"
|
||||
SecWebsocketExtensions = "Sec-Websocket-Extensions"
|
||||
SecWebsocketAccept = "Sec-Websocket-Accept"
|
||||
)
|
||||
|
||||
// HopHeaders Hop-by-hop headers. These are removed when sent to the backend.
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
|
||||
// Copied from reverseproxy.go, too bad
|
||||
var HopHeaders = []string{
|
||||
Connection,
|
||||
KeepAlive,
|
||||
ProxyAuthenticate,
|
||||
ProxyAuthorization,
|
||||
Te, // canonicalized version of "TE"
|
||||
Trailers,
|
||||
TransferEncoding,
|
||||
Upgrade,
|
||||
}
|
||||
|
||||
// WebsocketDialHeaders Websocket dial headers
|
||||
var WebsocketDialHeaders = []string{
|
||||
Upgrade,
|
||||
Connection,
|
||||
SecWebsocketKey,
|
||||
SecWebsocketVersion,
|
||||
SecWebsocketExtensions,
|
||||
SecWebsocketAccept,
|
||||
}
|
||||
|
||||
// WebsocketUpgradeHeaders Websocket upgrade headers
|
||||
var WebsocketUpgradeHeaders = []string{
|
||||
Upgrade,
|
||||
Connection,
|
||||
SecWebsocketAccept,
|
||||
SecWebsocketExtensions,
|
||||
}
|
||||
|
||||
// XHeaders X-* headers
|
||||
var XHeaders = []string{
|
||||
XForwardedProto,
|
||||
XForwardedFor,
|
||||
XForwardedHost,
|
||||
XForwardedPort,
|
||||
XForwardedServer,
|
||||
XRealIp,
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// +build go1.11
|
||||
|
||||
package forward
|
||||
|
||||
func (f *Forwarder) postConfig() {}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// +build !go1.11
|
||||
|
||||
package forward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type key string
|
||||
|
||||
const (
|
||||
teHeader key = "TeHeader"
|
||||
)
|
||||
|
||||
type TeTrailerRoundTripper struct {
|
||||
http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *TeTrailerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
teHeader := req.Context().Value(teHeader)
|
||||
if teHeader != nil {
|
||||
req.Header.Set("Te", teHeader.(string))
|
||||
}
|
||||
return t.RoundTripper.RoundTrip(req)
|
||||
}
|
||||
|
||||
type TeTrailerRewriter struct {
|
||||
ReqRewriter
|
||||
}
|
||||
|
||||
func (t *TeTrailerRewriter) Rewrite(req *http.Request) {
|
||||
if req.Header.Get("Te") == "trailers" {
|
||||
*req = *req.WithContext(context.WithValue(req.Context(), teHeader, req.Header.Get("Te")))
|
||||
}
|
||||
t.ReqRewriter.Rewrite(req)
|
||||
}
|
||||
|
||||
func (f *Forwarder) postConfig() {
|
||||
f.roundTripper = &TeTrailerRoundTripper{RoundTripper: f.roundTripper}
|
||||
f.rewriter = &TeTrailerRewriter{ReqRewriter: f.rewriter}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package forward
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/vulcand/oxy/utils"
|
||||
)
|
||||
|
||||
// HeaderRewriter is responsible for removing hop-by-hop headers and setting forwarding headers
|
||||
type HeaderRewriter struct {
|
||||
TrustForwardHeader bool
|
||||
Hostname string
|
||||
}
|
||||
|
||||
// clean up IP in case if it is ipv6 address and it has {zone} information in it, like "[fe80::d806:a55d:eb1b:49cc%vEthernet (vmxnet3 Ethernet Adapter - Virtual Switch)]:64692"
|
||||
func ipv6fix(clientIP string) string {
|
||||
return strings.Split(clientIP, "%")[0]
|
||||
}
|
||||
|
||||
// Rewrite rewrite request headers
|
||||
func (rw *HeaderRewriter) Rewrite(req *http.Request) {
|
||||
if !rw.TrustForwardHeader {
|
||||
utils.RemoveHeaders(req.Header, XHeaders...)
|
||||
}
|
||||
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
clientIP = ipv6fix(clientIP)
|
||||
// If not websocket, done in http.ReverseProxy
|
||||
if IsWebsocketRequest(req) {
|
||||
if prior, ok := req.Header[XForwardedFor]; ok {
|
||||
req.Header.Set(XForwardedFor, strings.Join(prior, ", ")+", "+clientIP)
|
||||
} else {
|
||||
req.Header.Set(XForwardedFor, clientIP)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Header.Get(XRealIp) == "" {
|
||||
req.Header.Set(XRealIp, clientIP)
|
||||
}
|
||||
}
|
||||
|
||||
xfProto := req.Header.Get(XForwardedProto)
|
||||
if xfProto == "" {
|
||||
if req.TLS != nil {
|
||||
req.Header.Set(XForwardedProto, "https")
|
||||
} else {
|
||||
req.Header.Set(XForwardedProto, "http")
|
||||
}
|
||||
}
|
||||
|
||||
if IsWebsocketRequest(req) {
|
||||
if req.Header.Get(XForwardedProto) == "https" {
|
||||
req.Header.Set(XForwardedProto, "wss")
|
||||
} else {
|
||||
req.Header.Set(XForwardedProto, "ws")
|
||||
}
|
||||
}
|
||||
|
||||
if xfPort := req.Header.Get(XForwardedPort); xfPort == "" {
|
||||
req.Header.Set(XForwardedPort, forwardedPort(req))
|
||||
}
|
||||
|
||||
if xfHost := req.Header.Get(XForwardedHost); xfHost == "" && req.Host != "" {
|
||||
req.Header.Set(XForwardedHost, req.Host)
|
||||
}
|
||||
|
||||
if rw.Hostname != "" {
|
||||
req.Header.Set(XForwardedServer, rw.Hostname)
|
||||
}
|
||||
}
|
||||
|
||||
func forwardedPort(req *http.Request) string {
|
||||
if req == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if _, port, err := net.SplitHostPort(req.Host); err == nil && port != "" {
|
||||
return port
|
||||
}
|
||||
|
||||
if req.Header.Get(XForwardedProto) == "https" || req.Header.Get(XForwardedProto) == "wss" {
|
||||
return "443"
|
||||
}
|
||||
|
||||
if req.TLS != nil {
|
||||
return "443"
|
||||
}
|
||||
|
||||
return "80"
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mailgun/timetools"
|
||||
"github.com/vulcand/oxy/utils"
|
||||
)
|
||||
|
||||
// NewHandler creates a new Server
|
||||
func NewHandler(handler http.HandlerFunc) *httptest.Server {
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
// NewResponder creates a new Server with response
|
||||
func NewResponder(response string) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(response))
|
||||
}))
|
||||
}
|
||||
|
||||
// ParseURI is the version of url.ParseRequestURI that panics if incorrect, helpful to shorten the tests
|
||||
func ParseURI(uri string) *url.URL {
|
||||
out, err := url.ParseRequestURI(uri)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ReqOpts request options
|
||||
type ReqOpts struct {
|
||||
Host string
|
||||
Method string
|
||||
Body string
|
||||
Headers http.Header
|
||||
Auth *utils.BasicAuth
|
||||
}
|
||||
|
||||
// ReqOption request option type
|
||||
type ReqOption func(o *ReqOpts) error
|
||||
|
||||
// Method sets request method
|
||||
func Method(m string) ReqOption {
|
||||
return func(o *ReqOpts) error {
|
||||
o.Method = m
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Host sets request host
|
||||
func Host(h string) ReqOption {
|
||||
return func(o *ReqOpts) error {
|
||||
o.Host = h
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Body sets request body
|
||||
func Body(b string) ReqOption {
|
||||
return func(o *ReqOpts) error {
|
||||
o.Body = b
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Header sets request header
|
||||
func Header(name, val string) ReqOption {
|
||||
return func(o *ReqOpts) error {
|
||||
if o.Headers == nil {
|
||||
o.Headers = make(http.Header)
|
||||
}
|
||||
o.Headers.Add(name, val)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Headers sets request headers
|
||||
func Headers(h http.Header) ReqOption {
|
||||
return func(o *ReqOpts) error {
|
||||
if o.Headers == nil {
|
||||
o.Headers = make(http.Header)
|
||||
}
|
||||
utils.CopyHeaders(o.Headers, h)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// BasicAuth sets request basic auth
|
||||
func BasicAuth(username, password string) ReqOption {
|
||||
return func(o *ReqOpts) error {
|
||||
o.Auth = &utils.BasicAuth{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MakeRequest create and do a request
|
||||
func MakeRequest(url string, opts ...ReqOption) (*http.Response, []byte, error) {
|
||||
o := &ReqOpts{}
|
||||
for _, s := range opts {
|
||||
if err := s(o); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if o.Method == "" {
|
||||
o.Method = http.MethodGet
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(o.Method, url, strings.NewReader(o.Body))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if o.Headers != nil {
|
||||
utils.CopyHeaders(request.Header, o.Headers)
|
||||
}
|
||||
|
||||
if o.Auth != nil {
|
||||
request.Header.Set("Authorization", o.Auth.String())
|
||||
}
|
||||
|
||||
if len(o.Host) != 0 {
|
||||
request.Host = o.Host
|
||||
}
|
||||
|
||||
var tr *http.Transport
|
||||
if strings.HasPrefix(url, "https") {
|
||||
tr = &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: request.Host,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
tr = &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return errors.New("no redirects")
|
||||
},
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err == nil {
|
||||
bodyBytes, errRead := ioutil.ReadAll(response.Body)
|
||||
return response, bodyBytes, errRead
|
||||
}
|
||||
return response, nil, err
|
||||
}
|
||||
|
||||
// Get do a GET request
|
||||
func Get(url string, opts ...ReqOption) (*http.Response, []byte, error) {
|
||||
opts = append(opts, Method(http.MethodGet))
|
||||
return MakeRequest(url, opts...)
|
||||
}
|
||||
|
||||
// Post do a POST request
|
||||
func Post(url string, opts ...ReqOption) (*http.Response, []byte, error) {
|
||||
opts = append(opts, Method(http.MethodPost))
|
||||
return MakeRequest(url, opts...)
|
||||
}
|
||||
|
||||
// GetClock gets a FreezedTime
|
||||
func GetClock() *timetools.FreezedTime {
|
||||
return &timetools.FreezedTime{
|
||||
CurrentTime: time.Date(2012, 3, 4, 5, 6, 7, 0, time.UTC),
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BasicAuth basic auth information
|
||||
type BasicAuth struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (ba *BasicAuth) String() string {
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", ba.Username, ba.Password)))
|
||||
return fmt.Sprintf("Basic %s", encoded)
|
||||
}
|
||||
|
||||
// ParseAuthHeader creates a new BasicAuth from header values
|
||||
func ParseAuthHeader(header string) (*BasicAuth, error) {
|
||||
values := strings.Fields(header)
|
||||
if len(values) != 2 {
|
||||
return nil, fmt.Errorf(fmt.Sprintf("Failed to parse header '%s'", header))
|
||||
}
|
||||
|
||||
authType := strings.ToLower(values[0])
|
||||
if authType != "basic" {
|
||||
return nil, fmt.Errorf("Expected basic auth type, got '%s'", authType)
|
||||
}
|
||||
|
||||
encodedString := values[1]
|
||||
decodedString, err := base64.StdEncoding.DecodeString(encodedString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse header '%s', base64 failed: %s", header, err)
|
||||
}
|
||||
|
||||
values = strings.SplitN(string(decodedString), ":", 2)
|
||||
if len(values) != 2 {
|
||||
return nil, fmt.Errorf("Failed to parse header '%s', expected separator ':'", header)
|
||||
}
|
||||
return &BasicAuth{Username: values[0], Password: values[1]}, nil
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// SerializableHttpRequest serializable HTTP request
|
||||
type SerializableHttpRequest struct {
|
||||
Method string
|
||||
URL *url.URL
|
||||
Proto string // "HTTP/1.0"
|
||||
ProtoMajor int // 1
|
||||
ProtoMinor int // 0
|
||||
Header http.Header
|
||||
ContentLength int64
|
||||
TransferEncoding []string
|
||||
Host string
|
||||
Form url.Values
|
||||
PostForm url.Values
|
||||
MultipartForm *multipart.Form
|
||||
Trailer http.Header
|
||||
RemoteAddr string
|
||||
RequestURI string
|
||||
TLS *tls.ConnectionState
|
||||
}
|
||||
|
||||
// Clone clone a request
|
||||
func Clone(r *http.Request) *SerializableHttpRequest {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rc := new(SerializableHttpRequest)
|
||||
rc.Method = r.Method
|
||||
rc.URL = r.URL
|
||||
rc.Proto = r.Proto
|
||||
rc.ProtoMajor = r.ProtoMajor
|
||||
rc.ProtoMinor = r.ProtoMinor
|
||||
rc.Header = r.Header
|
||||
rc.ContentLength = r.ContentLength
|
||||
rc.Host = r.Host
|
||||
rc.RemoteAddr = r.RemoteAddr
|
||||
rc.RequestURI = r.RequestURI
|
||||
return rc
|
||||
}
|
||||
|
||||
// ToJson serializes to JSON
|
||||
func (s *SerializableHttpRequest) ToJson() string {
|
||||
jsonVal, err := json.Marshal(s)
|
||||
if err != nil || jsonVal == nil {
|
||||
return fmt.Sprintf("Error marshalling SerializableHttpRequest to json: %s", err)
|
||||
}
|
||||
return string(jsonVal)
|
||||
}
|
||||
|
||||
// DumpHttpRequest dump a HTTP request to JSON
|
||||
func DumpHttpRequest(req *http.Request) string {
|
||||
return Clone(req).ToJson()
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// StatusClientClosedRequest non-standard HTTP status code for client disconnection
|
||||
const StatusClientClosedRequest = 499
|
||||
|
||||
// StatusClientClosedRequestText non-standard HTTP status for client disconnection
|
||||
const StatusClientClosedRequestText = "Client Closed Request"
|
||||
|
||||
// ErrorHandler error handler
|
||||
type ErrorHandler interface {
|
||||
ServeHTTP(w http.ResponseWriter, req *http.Request, err error)
|
||||
}
|
||||
|
||||
// DefaultHandler default error handler
|
||||
var DefaultHandler ErrorHandler = &StdHandler{}
|
||||
|
||||
// StdHandler Standard error handler
|
||||
type StdHandler struct{}
|
||||
|
||||
func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) {
|
||||
statusCode := http.StatusInternalServerError
|
||||
|
||||
if e, ok := err.(net.Error); ok {
|
||||
if e.Timeout() {
|
||||
statusCode = http.StatusGatewayTimeout
|
||||
} else {
|
||||
statusCode = http.StatusBadGateway
|
||||
}
|
||||
} else if err == io.EOF {
|
||||
statusCode = http.StatusBadGateway
|
||||
} else if err == context.Canceled {
|
||||
statusCode = StatusClientClosedRequest
|
||||
}
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
w.Write([]byte(statusText(statusCode)))
|
||||
log.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err)
|
||||
}
|
||||
|
||||
func statusText(statusCode int) string {
|
||||
if statusCode == StatusClientClosedRequest {
|
||||
return StatusClientClosedRequestText
|
||||
}
|
||||
return http.StatusText(statusCode)
|
||||
}
|
||||
|
||||
// ErrorHandlerFunc error handler function type
|
||||
type ErrorHandlerFunc func(http.ResponseWriter, *http.Request, error)
|
||||
|
||||
// ServeHTTP calls f(w, r).
|
||||
func (f ErrorHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, err error) {
|
||||
f(w, r, err)
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ProxyWriter calls recorder, used to debug logs
|
||||
type ProxyWriter struct {
|
||||
w http.ResponseWriter
|
||||
code int
|
||||
length int64
|
||||
|
||||
log *log.Logger
|
||||
}
|
||||
|
||||
// NewProxyWriter creates a new ProxyWriter
|
||||
func NewProxyWriter(w http.ResponseWriter) *ProxyWriter {
|
||||
return NewProxyWriterWithLogger(w, log.StandardLogger())
|
||||
}
|
||||
|
||||
// NewProxyWriterWithLogger creates a new ProxyWriter
|
||||
func NewProxyWriterWithLogger(w http.ResponseWriter, l *log.Logger) *ProxyWriter {
|
||||
return &ProxyWriter{
|
||||
w: w,
|
||||
log: l,
|
||||
}
|
||||
}
|
||||
|
||||
// StatusCode gets status code
|
||||
func (p *ProxyWriter) StatusCode() int {
|
||||
if p.code == 0 {
|
||||
// per contract standard lib will set this to http.StatusOK if not set
|
||||
// by user, here we avoid the confusion by mirroring this logic
|
||||
return http.StatusOK
|
||||
}
|
||||
return p.code
|
||||
}
|
||||
|
||||
// GetLength gets content length
|
||||
func (p *ProxyWriter) GetLength() int64 {
|
||||
return p.length
|
||||
}
|
||||
|
||||
// Header gets response header
|
||||
func (p *ProxyWriter) Header() http.Header {
|
||||
return p.w.Header()
|
||||
}
|
||||
|
||||
func (p *ProxyWriter) Write(buf []byte) (int, error) {
|
||||
p.length = p.length + int64(len(buf))
|
||||
return p.w.Write(buf)
|
||||
}
|
||||
|
||||
// WriteHeader writes status code
|
||||
func (p *ProxyWriter) WriteHeader(code int) {
|
||||
p.code = code
|
||||
p.w.WriteHeader(code)
|
||||
}
|
||||
|
||||
// Flush flush the writer
|
||||
func (p *ProxyWriter) Flush() {
|
||||
if f, ok := p.w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// CloseNotify returns a channel that receives at most a single value (true)
|
||||
// when the client connection has gone away.
|
||||
func (p *ProxyWriter) CloseNotify() <-chan bool {
|
||||
if cn, ok := p.w.(http.CloseNotifier); ok {
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
p.log.Debugf("Upstream ResponseWriter of type %v does not implement http.CloseNotifier. Returning dummy channel.", reflect.TypeOf(p.w))
|
||||
return make(<-chan bool)
|
||||
}
|
||||
|
||||
// Hijack lets the caller take over the connection.
|
||||
func (p *ProxyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hi, ok := p.w.(http.Hijacker); ok {
|
||||
return hi.Hijack()
|
||||
}
|
||||
p.log.Debugf("Upstream ResponseWriter of type %v does not implement http.Hijacker. Returning dummy channel.", reflect.TypeOf(p.w))
|
||||
return nil, nil, fmt.Errorf("the response writer that was wrapped in this proxy, does not implement http.Hijacker. It is of type: %v", reflect.TypeOf(p.w))
|
||||
}
|
||||
|
||||
// NewBufferWriter creates a new BufferWriter
|
||||
func NewBufferWriter(w io.WriteCloser) *BufferWriter {
|
||||
return &BufferWriter{
|
||||
W: w,
|
||||
H: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
// BufferWriter buffer writer
|
||||
type BufferWriter struct {
|
||||
H http.Header
|
||||
Code int
|
||||
W io.WriteCloser
|
||||
}
|
||||
|
||||
// Close close the writer
|
||||
func (b *BufferWriter) Close() error {
|
||||
return b.W.Close()
|
||||
}
|
||||
|
||||
// Header gets response header
|
||||
func (b *BufferWriter) Header() http.Header {
|
||||
return b.H
|
||||
}
|
||||
|
||||
func (b *BufferWriter) Write(buf []byte) (int, error) {
|
||||
return b.W.Write(buf)
|
||||
}
|
||||
|
||||
// WriteHeader writes status code
|
||||
func (b *BufferWriter) WriteHeader(code int) {
|
||||
b.Code = code
|
||||
}
|
||||
|
||||
// CloseNotify returns a channel that receives at most a single value (true)
|
||||
// when the client connection has gone away.
|
||||
func (b *BufferWriter) CloseNotify() <-chan bool {
|
||||
if cn, ok := b.W.(http.CloseNotifier); ok {
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
log.Warningf("Upstream ResponseWriter of type %v does not implement http.CloseNotifier. Returning dummy channel.", reflect.TypeOf(b.W))
|
||||
return make(<-chan bool)
|
||||
}
|
||||
|
||||
// Hijack lets the caller take over the connection.
|
||||
func (b *BufferWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hi, ok := b.W.(http.Hijacker); ok {
|
||||
return hi.Hijack()
|
||||
}
|
||||
log.Debugf("Upstream ResponseWriter of type %v does not implement http.Hijacker. Returning dummy channel.", reflect.TypeOf(b.W))
|
||||
return nil, nil, fmt.Errorf("the response writer that was wrapped in this proxy, does not implement http.Hijacker. It is of type: %v", reflect.TypeOf(b.W))
|
||||
}
|
||||
|
||||
type nopWriteCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (*nopWriteCloser) Close() error { return nil }
|
||||
|
||||
// NopWriteCloser returns a WriteCloser with a no-op Close method wrapping
|
||||
// the provided Writer w.
|
||||
func NopWriteCloser(w io.Writer) io.WriteCloser {
|
||||
return &nopWriteCloser{Writer: w}
|
||||
}
|
||||
|
||||
// CopyURL provides update safe copy by avoiding shallow copying User field
|
||||
func CopyURL(i *url.URL) *url.URL {
|
||||
out := *i
|
||||
if i.User != nil {
|
||||
out.User = &(*i.User)
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
// CopyHeaders copies http headers from source to destination, it
|
||||
// does not overide, but adds multiple headers
|
||||
func CopyHeaders(dst http.Header, src http.Header) {
|
||||
for k, vv := range src {
|
||||
dst[k] = append(dst[k], vv...)
|
||||
}
|
||||
}
|
||||
|
||||
// HasHeaders determines whether any of the header names is present in the http headers
|
||||
func HasHeaders(names []string, headers http.Header) bool {
|
||||
for _, h := range names {
|
||||
if headers.Get(h) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoveHeaders removes the header with the given names from the headers map
|
||||
func RemoveHeaders(headers http.Header, names ...string) {
|
||||
for _, h := range names {
|
||||
headers.Del(h)
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SourceExtractor extracts the source from the request, e.g. that may be client ip, or particular header that
|
||||
// identifies the source. amount stands for amount of connections the source consumes, usually 1 for connection limiters
|
||||
// error should be returned when source can not be identified
|
||||
type SourceExtractor interface {
|
||||
Extract(req *http.Request) (token string, amount int64, err error)
|
||||
}
|
||||
|
||||
// ExtractorFunc extractor function type
|
||||
type ExtractorFunc func(req *http.Request) (token string, amount int64, err error)
|
||||
|
||||
// Extract extract from request
|
||||
func (f ExtractorFunc) Extract(req *http.Request) (string, int64, error) {
|
||||
return f(req)
|
||||
}
|
||||
|
||||
// ExtractSource extract source function type
|
||||
type ExtractSource func(req *http.Request)
|
||||
|
||||
// NewExtractor creates a new SourceExtractor
|
||||
func NewExtractor(variable string) (SourceExtractor, error) {
|
||||
if variable == "client.ip" {
|
||||
return ExtractorFunc(extractClientIP), nil
|
||||
}
|
||||
if variable == "request.host" {
|
||||
return ExtractorFunc(extractHost), nil
|
||||
}
|
||||
if strings.HasPrefix(variable, "request.header.") {
|
||||
header := strings.TrimPrefix(variable, "request.header.")
|
||||
if len(header) == 0 {
|
||||
return nil, fmt.Errorf("wrong header: %s", header)
|
||||
}
|
||||
return makeHeaderExtractor(header), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported limiting variable: '%s'", variable)
|
||||
}
|
||||
|
||||
func extractClientIP(req *http.Request) (string, int64, error) {
|
||||
vals := strings.SplitN(req.RemoteAddr, ":", 2)
|
||||
if len(vals[0]) == 0 {
|
||||
return "", 0, fmt.Errorf("failed to parse client IP: %v", req.RemoteAddr)
|
||||
}
|
||||
return vals[0], 1, nil
|
||||
}
|
||||
|
||||
func extractHost(req *http.Request) (string, int64, error) {
|
||||
return req.Host, 1, nil
|
||||
}
|
||||
|
||||
func makeHeaderExtractor(header string) SourceExtractor {
|
||||
return ExtractorFunc(func(req *http.Request) (string, int64, error) {
|
||||
return req.Header.Get(header), 1, nil
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user