mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b8a533435 | |||
| 5abb90b539 | |||
| 0c8bc56930 | |||
| fdab68aa08 | |||
| 5aaab967a3 | |||
| ccad59dfab | |||
| 8a3eade6d3 | |||
| 39847a70f2 | |||
| d1e338ee48 | |||
| b243602d1c | |||
| 960c5a7baf | |||
| aca3575b6d | |||
| 2b4815a9f5 | |||
| 729890d847 | |||
| 31f424d589 | |||
| cb4bd8d065 | |||
| 1abd22ef0a | |||
| a3bcf25fae | |||
| 20e36c5bf3 | |||
| 5693ba524b | |||
| 9c6fbfca18 | |||
| 925ec100d6 | |||
| 58b27a1ccf |
@@ -1,3 +1,7 @@
|
||||
## 2023.7.0
|
||||
### New Features
|
||||
- You can now enable additional diagnostics over the management.argotunnel.com service for your active cloudflared connectors via a new runtime flag `--management-diagnostics` (or env `TUNNEL_MANAGEMENT_DIAGNOSTICS`). This feature is provided as opt-in and requires the flag to enable. Endpoints such as /metrics provides your prometheus metrics endpoint another mechanism to be reached. Additionally /debug/pprof/(goroutine|heap) are also introduced to allow for remotely retrieving active pprof information from a running cloudflared connector.
|
||||
|
||||
## 2023.4.1
|
||||
### New Features
|
||||
- You can now stream your logs from your remote cloudflared to your local terminal with `cloudflared tail <TUNNEL-ID>`. This new feature requires the remote cloudflared to be version 2023.4.1 or higher.
|
||||
|
||||
@@ -140,6 +140,7 @@ container:
|
||||
generate-docker-version:
|
||||
echo latest $(VERSION) > versions
|
||||
|
||||
|
||||
.PHONY: test
|
||||
test: vet
|
||||
ifndef CI
|
||||
@@ -147,9 +148,18 @@ ifndef CI
|
||||
else
|
||||
@mkdir -p .cover
|
||||
go test -v -mod=vendor -race $(LDFLAGS) -coverprofile=".cover/c.out" ./...
|
||||
go tool cover -html ".cover/c.out" -o .cover/all.html
|
||||
endif
|
||||
|
||||
.PHONY: cover
|
||||
cover:
|
||||
@echo ""
|
||||
@echo "=====> Total test coverage: <====="
|
||||
@echo ""
|
||||
# Print the overall coverage here for quick access.
|
||||
$Q go tool cover -func ".cover/c.out" | grep "total:" | awk '{print $$3}'
|
||||
# Generate the HTML report that can be viewed from the browser in CI.
|
||||
$Q go tool cover -html ".cover/c.out" -o .cover/all.html
|
||||
|
||||
.PHONY: test-ssh-server
|
||||
test-ssh-server:
|
||||
docker-compose -f ssh_server_tests/docker-compose.yml up
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
2023.7.1
|
||||
- 2023-07-13 TUN-7582: Correct changelog wording for --management-diagnostics
|
||||
- 2023-07-12 TUN-7575: Add option to disable PTMU discovery over QUIC
|
||||
|
||||
2023.7.0
|
||||
- 2023-07-06 TUN-7558: Flush on Writes for StreamBasedOriginProxy
|
||||
- 2023-07-05 TUN-7553: Add flag to enable management diagnostic services
|
||||
- 2023-07-05 TUN-7564: Support cf-trace-id for cloudflared access
|
||||
- 2023-07-05 TUN-7477: Decrement UDP sessions on shutdown
|
||||
- 2023-07-03 TUN-7545: Add support for full bidirectionally streaming with close signal propagation
|
||||
- 2023-06-30 TUN-7549: Add metrics route to management service
|
||||
- 2023-06-30 TUN-7551: Complete removal of raven-go to sentry-go
|
||||
- 2023-06-30 TUN-7550: Add pprof endpoint to management service
|
||||
- 2023-06-29 TUN-7543: Add --debug-stream flag to cloudflared access ssh
|
||||
- 2023-06-26 TUN-6011: Remove docker networks from ICMP Proxy test
|
||||
- 2023-06-20 AUTH-5328 Pass cloudflared_token_check param when running cloudflared access login
|
||||
|
||||
2023.6.1
|
||||
- 2023-06-19 TUN-7480: Added a timeout for unregisterUDP.
|
||||
- 2023-06-16 TUN-7477: Add UDP/TCP session metrics
|
||||
- 2023-06-14 TUN-7468: Increase the limit of incoming streams
|
||||
|
||||
2023.6.0
|
||||
- 2023-06-15 TUN-7471: Fixes cloudflared not closing the quic stream on unregister UDP session
|
||||
- 2023-06-09 TUN-7463: Add default ingress rule if no ingress rules are provided when updating the configuration
|
||||
- 2023-05-31 TUN-7447: Add a cover build to report code coverage
|
||||
|
||||
2023.5.1
|
||||
- 2023-05-16 TUN-7424: Add CORS headers to host_details responses
|
||||
- 2023-05-11 TUN-7421: Add *.cloudflare.com to permitted Origins for management WebSocket requests
|
||||
|
||||
@@ -29,6 +29,12 @@ buster: &buster
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared
|
||||
cover:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deps
|
||||
pre-cache: *build_pre_cache
|
||||
post-cache:
|
||||
- make cover
|
||||
# except FIPS (handled in github-fips-release-pkgs) and macos (handled in github-release-macos-amd64)
|
||||
github-release-pkgs:
|
||||
build_dir: *build_dir
|
||||
|
||||
@@ -3,6 +3,7 @@ package access
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/stream"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
@@ -38,6 +40,7 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
|
||||
if forwarder.TokenSecret != "" {
|
||||
headers.Set(cfAccessClientSecretHeader, forwarder.TokenSecret)
|
||||
}
|
||||
headers.Set("User-Agent", userAgent)
|
||||
|
||||
carrier.SetBastionDest(headers, forwarder.Destination)
|
||||
|
||||
@@ -58,7 +61,12 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
|
||||
// useful for proxying other protocols (like ssh) over websockets
|
||||
// (which you can put Access in front of)
|
||||
func ssh(c *cli.Context) error {
|
||||
log := logger.CreateSSHLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
// If not running as a forwarder, disable terminal logs as it collides with the stdin/stdout of the parent process
|
||||
outputTerminal := logger.DisableTerminalLog
|
||||
if c.IsSet(sshURLFlag) {
|
||||
outputTerminal = logger.EnableTerminalLog
|
||||
}
|
||||
log := logger.CreateSSHLoggerFromContext(c, outputTerminal)
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
@@ -76,6 +84,7 @@ func ssh(c *cli.Context) error {
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Set(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
}
|
||||
headers.Set("User-Agent", userAgent)
|
||||
|
||||
carrier.SetBastionDest(headers, c.String(sshDestinationFlag))
|
||||
|
||||
@@ -121,15 +130,27 @@ func ssh(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return carrier.StartClient(wsConn, &carrier.StdinoutStream{}, options)
|
||||
var s io.ReadWriter
|
||||
s = &carrier.StdinoutStream{}
|
||||
if c.IsSet(sshDebugStream) {
|
||||
maxMessages := c.Uint64(sshDebugStream)
|
||||
if maxMessages == 0 {
|
||||
// default to 10 if provided but unset
|
||||
maxMessages = 10
|
||||
}
|
||||
logger := log.With().Str("host", hostname).Logger()
|
||||
s = stream.NewDebugStream(s, &logger, maxMessages)
|
||||
}
|
||||
carrier.StartClient(wsConn, s, options)
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRequestHeaders(values []string) http.Header {
|
||||
headers := make(http.Header)
|
||||
for _, valuePair := range values {
|
||||
split := strings.Split(valuePair, ":")
|
||||
if len(split) > 1 {
|
||||
headers.Add(strings.TrimSpace(split[0]), strings.TrimSpace(split[1]))
|
||||
header, value, found := strings.Cut(valuePair, ":")
|
||||
if found {
|
||||
headers.Add(strings.TrimSpace(header), strings.TrimSpace(value))
|
||||
}
|
||||
}
|
||||
return headers
|
||||
|
||||
@@ -12,7 +12,8 @@ func TestBuildRequestHeaders(t *testing.T) {
|
||||
headers.Add("client", "value")
|
||||
headers.Add("secret", "safe-value")
|
||||
|
||||
values := buildRequestHeaders([]string{"client: value", "secret: safe-value", "trash"})
|
||||
values := buildRequestHeaders([]string{"client: value", "secret: safe-value", "trash", "cf-trace-id: 000:000:0:1:asd"})
|
||||
assert.Equal(t, headers.Get("client"), values.Get("client"))
|
||||
assert.Equal(t, headers.Get("secret"), values.Get("secret"))
|
||||
assert.Equal(t, headers.Get("cf-trace-id"), values.Get("000:000:0:1:asd"))
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ const (
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshConnectTo = "connect-to"
|
||||
sshDebugStream = "debug-stream"
|
||||
sshConfigTemplate = `
|
||||
Add to your {{.Home}}/.ssh/config:
|
||||
|
||||
@@ -151,9 +152,12 @@ func Commands() []*cli.Command {
|
||||
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logger.LogSSHDirectoryFlag,
|
||||
Aliases: []string{"logfile"}, //added to match the tunnel side
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
Name: logger.LogFileFlag,
|
||||
Usage: "Save application log to this file for reporting issues.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logger.LogSSHDirectoryFlag,
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logger.LogSSHLevelFlag,
|
||||
@@ -165,6 +169,11 @@ func Commands() []*cli.Command {
|
||||
Hidden: true,
|
||||
Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host",
|
||||
},
|
||||
&cli.Uint64Flag{
|
||||
Name: sshDebugStream,
|
||||
Hidden: true,
|
||||
Usage: "Writes up-to the max provided stream payloads to the logger as debug statements.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -525,6 +534,11 @@ func isTokenValid(options *carrier.StartOptions, log *zerolog.Logger) (bool, err
|
||||
return false, errors.Wrap(err, "Could not create access request")
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Set("cloudflared_token_check", "true")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
// Do not follow redirects
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
|
||||
@@ -79,6 +79,14 @@ const (
|
||||
// hostKeyPath is the path of the dir to save SSH host keys too
|
||||
hostKeyPath = "host-key-path"
|
||||
|
||||
// udpUnregisterSessionTimeout is how long we wait before we stop trying to unregister a UDP session from the edge
|
||||
udpUnregisterSessionTimeoutFlag = "udp-unregister-session-timeout"
|
||||
|
||||
// quicDisablePathMTUDiscovery sets if QUIC should not perform PTMU discovery and use a smaller (safe) packet size.
|
||||
// Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
|
||||
// Note that this may result in packet drops for UDP proxying, since we expect being able to send at least 1280 bytes of inner packets.
|
||||
quicDisablePathMTUDiscovery = "quic-disable-pmtu-discovery"
|
||||
|
||||
// uiFlag is to enable launching cloudflared in interactive UI mode
|
||||
uiFlag = "ui"
|
||||
|
||||
@@ -399,7 +407,7 @@ func StartServer(
|
||||
}
|
||||
}
|
||||
|
||||
localRules := []ingress.Rule{}
|
||||
internalRules := []ingress.Rule{}
|
||||
if features.Contains(features.FeatureManagementLogs) {
|
||||
serviceIP := c.String("service-op-ip")
|
||||
if edgeAddrs, err := edgediscovery.ResolveEdge(log, tunnelConfig.Region, tunnelConfig.EdgeIPVersion); err == nil {
|
||||
@@ -410,15 +418,16 @@ func StartServer(
|
||||
|
||||
mgmt := management.New(
|
||||
c.String("management-hostname"),
|
||||
c.Bool("management-diagnostics"),
|
||||
serviceIP,
|
||||
clientID,
|
||||
c.String(connectorLabelFlag),
|
||||
logger.ManagementLogger.Log,
|
||||
logger.ManagementLogger,
|
||||
)
|
||||
localRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
|
||||
internalRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
|
||||
}
|
||||
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, localRules, tunnelConfig.Log)
|
||||
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, internalRules, tunnelConfig.Log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -683,6 +692,18 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Value: 4,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: udpUnregisterSessionTimeoutFlag,
|
||||
Value: 5 * time.Second,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: quicDisablePathMTUDiscovery,
|
||||
EnvVars: []string{"TUNNEL_DISABLE_QUIC_PMTU"},
|
||||
Usage: "Use this option to disable PTMU discovery for QUIC connections. This will result in lower packet sizes. Not however, that this may cause instability for UDP proxying.",
|
||||
Value: false,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: connectorLabelFlag,
|
||||
Usage: "Use this option to give a meaningful label to a specific connector. When a tunnel starts up, a connector id unique to the tunnel is generated. This is a uuid. To make it easier to identify a connector, we will use the hostname of the machine the tunnel is running on along with the connector ID. This option exists if one wants to have more control over what their individual connectors are called.",
|
||||
@@ -756,6 +777,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_POST_QUANTUM"},
|
||||
Hidden: FipsEnabled,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "management-diagnostics",
|
||||
Usage: "Enables the in-depth diagnostic routes to be made available over the management service (/debug/pprof, /metrics, etc.)",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_DIAGNOSTICS"},
|
||||
Value: false,
|
||||
}),
|
||||
selectProtocolFlag,
|
||||
overwriteDNSFlag,
|
||||
}...)
|
||||
|
||||
@@ -231,14 +231,16 @@ func prepareTunnelConfig(
|
||||
Observer: observer,
|
||||
ReportedVersion: info.Version(),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
Retries: uint(c.Int("retries")),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
NamedTunnel: namedTunnel,
|
||||
ProtocolSelector: protocolSelector,
|
||||
EdgeTLSConfigs: edgeTLSConfigs,
|
||||
NeedPQ: needPQ,
|
||||
PQKexIdx: pqKexIdx,
|
||||
MaxEdgeAddrRetries: uint8(c.Int("max-edge-addr-retries")),
|
||||
Retries: uint(c.Int("retries")),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
NamedTunnel: namedTunnel,
|
||||
ProtocolSelector: protocolSelector,
|
||||
EdgeTLSConfigs: edgeTLSConfigs,
|
||||
NeedPQ: needPQ,
|
||||
PQKexIdx: pqKexIdx,
|
||||
MaxEdgeAddrRetries: uint8(c.Int("max-edge-addr-retries")),
|
||||
UDPUnregisterSessionTimeout: c.Duration(udpUnregisterSessionTimeoutFlag),
|
||||
DisableQUICPathMTUDiscovery: c.Bool(quicDisablePathMTUDiscovery),
|
||||
}
|
||||
packetConfig, err := newPacketConfig(c, log)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python
|
||||
import requests
|
||||
from conftest import CfdModes
|
||||
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
|
||||
from retrying import retry
|
||||
from cli import CloudflaredCli
|
||||
from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests
|
||||
import platform
|
||||
|
||||
"""
|
||||
Each test in TestManagement will:
|
||||
1. Acquire a management token from Cloudflare public API
|
||||
2. Make a request against the management service for the running tunnel
|
||||
"""
|
||||
class TestManagement:
|
||||
"""
|
||||
test_get_host_details does the following:
|
||||
1. It gets a management token from Tunnelstore using cloudflared tail token <tunnel_id>
|
||||
2. It gets the connector_id after starting a cloudflare tunnel
|
||||
3. It sends a request to the management host with the connector_id and management token
|
||||
4. Asserts that the response has a hostname and ip.
|
||||
"""
|
||||
def test_get_host_details(self, tmp_path, component_tests_config):
|
||||
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
headers = {}
|
||||
headers["Content-Type"] = "application/json"
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--label" , "test"], cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
connector_id = cfd_cli.get_connector_id(config)[0]
|
||||
url = cfd_cli.get_management_url("host_details", config, config_path)
|
||||
resp = send_request(url, headers=headers)
|
||||
|
||||
# Assert response json.
|
||||
assert resp.status_code == 200, "Expected cloudflared to return 200 for host details"
|
||||
assert resp.json()["hostname"] == "custom:test", "Expected cloudflared to return hostname"
|
||||
assert resp.json()["ip"] != "", "Expected cloudflared to return ip"
|
||||
assert resp.json()["connector_id"] == connector_id, "Expected cloudflared to return connector_id"
|
||||
|
||||
"""
|
||||
test_get_metrics will verify that the /metrics endpoint returns the prometheus metrics dump
|
||||
"""
|
||||
def test_get_metrics(self, tmp_path, component_tests_config):
|
||||
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--management-diagnostics"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_url("metrics", config, config_path)
|
||||
resp = send_request(url)
|
||||
|
||||
# Assert response.
|
||||
assert resp.status_code == 200, "Expected cloudflared to return 200 for /metrics"
|
||||
assert "# HELP build_info Build and version information" in resp.text, "Expected /metrics to have with the build_info details"
|
||||
|
||||
"""
|
||||
test_get_pprof_heap will verify that the /debug/pprof/heap endpoint returns a pprof/heap dump response
|
||||
"""
|
||||
def test_get_pprof_heap(self, tmp_path, component_tests_config):
|
||||
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--management-diagnostics"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_url("debug/pprof/heap", config, config_path)
|
||||
resp = send_request(url)
|
||||
|
||||
# Assert response.
|
||||
assert resp.status_code == 200, "Expected cloudflared to return 200 for /debug/pprof/heap"
|
||||
assert resp.headers["Content-Type"] == "application/octet-stream", "Expected /debug/pprof/heap to have return a binary response"
|
||||
|
||||
"""
|
||||
test_get_metrics_when_disabled will verify that diagnostic endpoints (such as /metrics) return 404 and are unmounted.
|
||||
"""
|
||||
def test_get_metrics_when_disabled(self, tmp_path, component_tests_config):
|
||||
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_url("metrics", config, config_path)
|
||||
resp = send_request(url)
|
||||
|
||||
# Assert response.
|
||||
assert resp.status_code == 404, "Expected cloudflared to return 404 for /metrics"
|
||||
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def send_request(url, headers={}):
|
||||
with requests.Session() as s:
|
||||
return s.get(url, timeout=BACKOFF_SECS, headers=headers)
|
||||
@@ -16,38 +16,6 @@ class TestTunnel:
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=1)
|
||||
|
||||
"""
|
||||
test_get_host_details does the following:
|
||||
1. It gets a management token from Tunnelstore using cloudflared tail token <tunnel_id>
|
||||
2. It gets the connector_id after starting a cloudflare tunnel
|
||||
3. It sends a request to the management host with the connector_id and management token
|
||||
4. Asserts that the response has a hostname and ip.
|
||||
"""
|
||||
def test_get_host_details(self, tmp_path, component_tests_config):
|
||||
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
headers = {}
|
||||
headers["Content-Type"] = "application/json"
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--label" , "test"], cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
connector_id = cfd_cli.get_connector_id(config)[0]
|
||||
url = cfd_cli.get_management_url("host_details", config, config_path)
|
||||
resp = send_request(url, headers=headers)
|
||||
|
||||
# Assert response json.
|
||||
assert resp.status_code == 200, "Expected cloudflared to return 200 for host details"
|
||||
assert resp.json()["hostname"] == "custom:test", "Expected cloudflared to return hostname"
|
||||
assert resp.json()["ip"] != "", "Expected cloudflared to return ip"
|
||||
assert resp.json()["connector_id"] == connector_id, "Expected cloudflared to return connector_id"
|
||||
|
||||
|
||||
def test_tunnel_url(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
|
||||
@@ -157,14 +157,16 @@ type ReadWriteAcker interface {
|
||||
type HTTPResponseReadWriteAcker struct {
|
||||
r io.Reader
|
||||
w ResponseWriter
|
||||
f http.Flusher
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
// NewHTTPResponseReadWriterAcker returns a new instance of HTTPResponseReadWriteAcker.
|
||||
func NewHTTPResponseReadWriterAcker(w ResponseWriter, req *http.Request) *HTTPResponseReadWriteAcker {
|
||||
func NewHTTPResponseReadWriterAcker(w ResponseWriter, flusher http.Flusher, req *http.Request) *HTTPResponseReadWriteAcker {
|
||||
return &HTTPResponseReadWriteAcker{
|
||||
r: req.Body,
|
||||
w: w,
|
||||
f: flusher,
|
||||
req: req,
|
||||
}
|
||||
}
|
||||
@@ -174,7 +176,11 @@ func (h *HTTPResponseReadWriteAcker) Read(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (h *HTTPResponseReadWriteAcker) Write(p []byte) (int, error) {
|
||||
return h.w.Write(p)
|
||||
n, err := h.w.Write(p)
|
||||
if n > 0 {
|
||||
h.f.Flush()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// AckConnection acks an HTTP connection by sending a switch protocols status code that enables the caller to
|
||||
|
||||
@@ -130,7 +130,8 @@ func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
wsCtx, cancel := context.WithCancel(r.Context())
|
||||
readPipe, writePipe := io.Pipe()
|
||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
|
||||
|
||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log)
|
||||
go func() {
|
||||
select {
|
||||
case <-wsCtx.Done():
|
||||
@@ -175,7 +176,7 @@ func wsFlakyEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
wsCtx, cancel := context.WithCancel(r.Context())
|
||||
|
||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
|
||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log)
|
||||
|
||||
closedAfter := time.Millisecond * time.Duration(rand.Intn(50))
|
||||
originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)}
|
||||
|
||||
+5
-1
@@ -142,7 +142,7 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
break
|
||||
}
|
||||
|
||||
rws := NewHTTPResponseReadWriterAcker(respWriter, r)
|
||||
rws := NewHTTPResponseReadWriterAcker(respWriter, respWriter, r)
|
||||
requestErr = originProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
|
||||
Dest: host,
|
||||
CFRay: FindCfRayHeader(r),
|
||||
@@ -289,6 +289,10 @@ func (rp *http2RespWriter) Header() http.Header {
|
||||
return rp.respHeaders
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) Flush() {
|
||||
rp.flusher.Flush()
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) WriteHeader(status int) {
|
||||
if rp.hijacked() {
|
||||
rp.log.Warn().Msg("WriteHeader after hijack")
|
||||
|
||||
+22
-2
@@ -63,6 +63,8 @@ type QUICConnection struct {
|
||||
controlStreamHandler ControlStreamHandler
|
||||
connOptions *tunnelpogs.ConnectionOptions
|
||||
connIndex uint8
|
||||
|
||||
udpUnregisterTimeout time.Duration
|
||||
}
|
||||
|
||||
// NewQUICConnection returns a new instance of QUICConnection.
|
||||
@@ -78,6 +80,7 @@ func NewQUICConnection(
|
||||
controlStreamHandler ControlStreamHandler,
|
||||
logger *zerolog.Logger,
|
||||
packetRouterConfig *ingress.GlobalRouterConfig,
|
||||
udpUnregisterTimeout time.Duration,
|
||||
) (*QUICConnection, error) {
|
||||
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, logger)
|
||||
if err != nil {
|
||||
@@ -112,6 +115,7 @@ func NewQUICConnection(
|
||||
controlStreamHandler: controlStreamHandler,
|
||||
connOptions: connOptions,
|
||||
connIndex: connIndex,
|
||||
udpUnregisterTimeout: udpUnregisterTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -357,7 +361,7 @@ func (q *QUICConnection) serveUDPSession(session *datagramsession.Session, close
|
||||
// closeUDPSession first unregisters the session from session manager, then it tries to unregister from edge
|
||||
func (q *QUICConnection) closeUDPSession(ctx context.Context, sessionID uuid.UUID, message string) {
|
||||
q.sessionManager.UnregisterSession(ctx, sessionID, message, false)
|
||||
stream, err := q.session.OpenStream()
|
||||
quicStream, err := q.session.OpenStream()
|
||||
if err != nil {
|
||||
// Log this at debug because this is not an error if session was closed due to lost connection
|
||||
// with edge
|
||||
@@ -367,7 +371,10 @@ func (q *QUICConnection) closeUDPSession(ctx context.Context, sessionID uuid.UUI
|
||||
Msgf("Failed to open quic stream to unregister udp session with edge")
|
||||
return
|
||||
}
|
||||
rpcClientStream, err := quicpogs.NewRPCClientStream(ctx, stream, q.logger)
|
||||
|
||||
stream := quicpogs.NewSafeStreamCloser(quicStream)
|
||||
defer stream.Close()
|
||||
rpcClientStream, err := quicpogs.NewRPCClientStream(ctx, stream, q.udpUnregisterTimeout, q.logger)
|
||||
if err != nil {
|
||||
// Log this at debug because this is not an error if session was closed due to lost connection
|
||||
// with edge
|
||||
@@ -375,6 +382,8 @@ func (q *QUICConnection) closeUDPSession(ctx context.Context, sessionID uuid.UUI
|
||||
Msgf("Failed to open rpc stream to unregister udp session with edge")
|
||||
return
|
||||
}
|
||||
defer rpcClientStream.Close()
|
||||
|
||||
if err := rpcClientStream.UnregisterUdpSession(ctx, sessionID, message); err != nil {
|
||||
q.logger.Err(err).Str("sessionID", sessionID.String()).
|
||||
Msgf("Failed to unregister udp session with edge")
|
||||
@@ -440,10 +449,21 @@ func (hrw *httpResponseAdapter) WriteRespHeaders(status int, header http.Header)
|
||||
return hrw.WriteConnectResponseData(nil, metadata...)
|
||||
}
|
||||
|
||||
func (hrw *httpResponseAdapter) Write(p []byte) (int, error) {
|
||||
// Make sure to send WriteHeader response if not called yet
|
||||
if !hrw.connectResponseSent {
|
||||
hrw.WriteRespHeaders(http.StatusOK, hrw.headers)
|
||||
}
|
||||
return hrw.RequestServerStream.Write(p)
|
||||
}
|
||||
|
||||
func (hrw *httpResponseAdapter) Header() http.Header {
|
||||
return hrw.headers
|
||||
}
|
||||
|
||||
// This is a no-op Flush because this adapter is over a quic.Stream and we don't need Flush here.
|
||||
func (hrw *httpResponseAdapter) Flush() {}
|
||||
|
||||
func (hrw *httpResponseAdapter) WriteHeader(status int) {
|
||||
hrw.WriteRespHeaders(status, hrw.headers)
|
||||
}
|
||||
|
||||
@@ -725,6 +725,7 @@ func testQUICConnection(udpListenerAddr net.Addr, t *testing.T, index uint8) *QU
|
||||
fakeControlStream{},
|
||||
&log,
|
||||
nil,
|
||||
5*time.Second,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return qc
|
||||
|
||||
@@ -92,7 +92,10 @@ func (m *manager) shutdownSessions(err error) {
|
||||
byRemote: true,
|
||||
}
|
||||
for _, s := range m.sessions {
|
||||
s.close(closeSessionErr)
|
||||
m.unregisterSession(&unregisterSessionEvent{
|
||||
sessionID: s.ID,
|
||||
err: closeSessionErr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +121,7 @@ func (m *manager) registerSession(ctx context.Context, registration *registerSes
|
||||
session := m.newSession(registration.sessionID, registration.originProxy)
|
||||
m.sessions[registration.sessionID] = session
|
||||
registration.resultChan <- session
|
||||
incrementUDPSessions()
|
||||
}
|
||||
|
||||
func (m *manager) newSession(id uuid.UUID, dstConn io.ReadWriteCloser) *Session {
|
||||
@@ -163,6 +167,7 @@ func (m *manager) unregisterSession(unregistration *unregisterSessionEvent) {
|
||||
if ok {
|
||||
delete(m.sessions, unregistration.sessionID)
|
||||
session.close(unregistration.err)
|
||||
decrementUDPActiveSessions()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package datagramsession
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "cloudflared"
|
||||
)
|
||||
|
||||
var (
|
||||
activeUDPSessions = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "udp",
|
||||
Name: "active_sessions",
|
||||
Help: "Concurrent count of UDP sessions that are being proxied to any origin",
|
||||
})
|
||||
totalUDPSessions = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "udp",
|
||||
Name: "total_sessions",
|
||||
Help: "Total count of UDP sessions that have been proxied to any origin",
|
||||
})
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(
|
||||
activeUDPSessions,
|
||||
totalUDPSessions,
|
||||
)
|
||||
}
|
||||
|
||||
func incrementUDPSessions() {
|
||||
totalUDPSessions.Inc()
|
||||
activeUDPSessions.Inc()
|
||||
}
|
||||
|
||||
func decrementUDPActiveSessions() {
|
||||
activeUDPSessions.Dec()
|
||||
}
|
||||
@@ -10,9 +10,9 @@ require (
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/getsentry/raven-go v0.2.0
|
||||
github.com/getsentry/sentry-go v0.16.0
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-jose/go-jose/v3 v3.0.0
|
||||
github.com/gobwas/ws v1.0.4
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
@@ -55,7 +55,6 @@ require (
|
||||
github.com/BurntSushi/toml v1.2.0 // indirect
|
||||
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cloudflare/circl v1.2.1-0.20220809205628-0a9554f37a47 // indirect
|
||||
github.com/coredns/caddy v1.1.1 // indirect
|
||||
@@ -66,7 +65,6 @@ require (
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/go-chi/cors v1.2.1 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
|
||||
@@ -74,8 +74,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
github.com/bwesterb/go-ristretto v1.2.2/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
@@ -145,8 +143,6 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjr
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/getsentry/sentry-go v0.16.0 h1:owk+S+5XcgJLlGR/3+3s6N4d+uKwqYvh/eS0AIMjPWo=
|
||||
github.com/getsentry/sentry-go v0.16.0/go.mod h1:ZXCloQLj0pG7mja5NK6NPf2V4A88YJ4pNlc2mOHwh6Y=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
|
||||
+19
-10
@@ -44,7 +44,7 @@ func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) {
|
||||
if err == nil {
|
||||
hostname = host
|
||||
}
|
||||
for i, rule := range ing.LocalRules {
|
||||
for i, rule := range ing.InternalRules {
|
||||
if rule.Matches(hostname, path) {
|
||||
// Local rule matches return a negative rule index to distiguish local rules from user-defined rules in logs
|
||||
// Full range would be [-1 .. )
|
||||
@@ -77,7 +77,7 @@ func matchHost(ruleHost, reqHost string) bool {
|
||||
// Ingress maps eyeball requests to origins.
|
||||
type Ingress struct {
|
||||
// Set of ingress rules that are not added to remote config, e.g. management
|
||||
LocalRules []Rule
|
||||
InternalRules []Rule
|
||||
// Rules that are provided by the user from remote or local configuration
|
||||
Rules []Rule `json:"ingress"`
|
||||
Defaults OriginRequestConfig `json:"originRequest"`
|
||||
@@ -110,16 +110,18 @@ func ParseIngressFromConfigAndCLI(conf *config.Configuration, c *cli.Context, lo
|
||||
// --bastion for ssh bastion service
|
||||
ingressRules, err = parseCLIIngress(c, false)
|
||||
if errors.Is(err, ErrNoIngressRulesCLI) {
|
||||
// Only log a warning if the tunnel is not a remotely managed tunnel and the config
|
||||
// will be loaded after connecting.
|
||||
// If no token is provided, the probability of NOT being a remotely managed tunnel is higher.
|
||||
// So, we should warn the user that no ingress rules were found, because remote configuration will most likely not exist.
|
||||
if !c.IsSet("token") {
|
||||
log.Warn().Msgf(ErrNoIngressRulesCLI.Error())
|
||||
}
|
||||
return newDefaultOrigin(c, log), nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return Ingress{}, err
|
||||
}
|
||||
|
||||
return ingressRules, nil
|
||||
}
|
||||
|
||||
@@ -148,14 +150,10 @@ func parseCLIIngress(c *cli.Context, allowURLFromArgs bool) (Ingress, error) {
|
||||
// newDefaultOrigin always returns a 503 response code to help indicate that there are no ingress
|
||||
// rules setup, but the tunnel is reachable.
|
||||
func newDefaultOrigin(c *cli.Context, log *zerolog.Logger) Ingress {
|
||||
noRulesService := newDefaultStatusCode(log)
|
||||
defaultRule := GetDefaultIngressRules(log)
|
||||
defaults := originRequestFromSingleRule(c)
|
||||
ingress := Ingress{
|
||||
Rules: []Rule{
|
||||
{
|
||||
Service: &noRulesService,
|
||||
},
|
||||
},
|
||||
Rules: defaultRule,
|
||||
Defaults: defaults,
|
||||
}
|
||||
return ingress
|
||||
@@ -219,6 +217,17 @@ func (ing Ingress) CatchAll() *Rule {
|
||||
return &ing.Rules[len(ing.Rules)-1]
|
||||
}
|
||||
|
||||
// Gets the default ingress rule that will be return 503 status
|
||||
// code for all incoming requests.
|
||||
func GetDefaultIngressRules(log *zerolog.Logger) []Rule {
|
||||
noRulesService := newDefaultStatusCode(log)
|
||||
return []Rule{
|
||||
{
|
||||
Service: &noRulesService,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func validateAccessConfiguration(cfg *config.AccessConfig) error {
|
||||
if !cfg.Required {
|
||||
return nil
|
||||
|
||||
@@ -390,15 +390,16 @@ func getLocalIPs(t *testing.T, ipv4 bool) []netip.Addr {
|
||||
require.NoError(t, err)
|
||||
localIPs := []netip.Addr{}
|
||||
for _, i := range interfaces {
|
||||
// Skip TUN devices
|
||||
if strings.Contains(i.Name, "tun") {
|
||||
// Skip TUN devices, and Docker Networks
|
||||
if strings.Contains(i.Name, "tun") || strings.Contains(i.Name, "docker") || strings.HasPrefix(i.Name, "br-") {
|
||||
continue
|
||||
}
|
||||
addrs, err := i.Addrs()
|
||||
require.NoError(t, err)
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && (ipnet.IP.IsPrivate() || ipnet.IP.IsLoopback()) {
|
||||
if (ipv4 && ipnet.IP.To4() != nil) || (!ipv4 && ipnet.IP.To4() == nil) {
|
||||
// TODO DEVTOOLS-12514: We only run the IPv6 against the loopback interface due to issues on the CI runners.
|
||||
if (ipv4 && ipnet.IP.To4() != nil) || (!ipv4 && ipnet.IP.To4() == nil && ipnet.IP.IsLoopback()) {
|
||||
localIPs = append(localIPs, netip.MustParseAddr(ipnet.IP.String()))
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ func createFromContext(
|
||||
|
||||
log := newZerolog(loggerConfig)
|
||||
if incompatibleFlagsSet := logFile != "" && logDirectory != ""; incompatibleFlagsSet {
|
||||
log.Error().Msgf("Your config includes values for both %s and %s, but they are incompatible. %s takes precedence.", LogFileFlag, logDirectoryFlagName, LogFileFlag)
|
||||
log.Error().Msgf("Your config includes values for both %s (%s) and %s (%s), but they are incompatible. %s takes precedence.", LogFileFlag, logFile, logDirectoryFlagName, logDirectory, LogFileFlag)
|
||||
}
|
||||
return log
|
||||
}
|
||||
|
||||
+38
-19
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog"
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
@@ -29,6 +31,17 @@ const (
|
||||
reasonIdleLimitExceeded = "session was idle for too long"
|
||||
)
|
||||
|
||||
var (
|
||||
// CORS middleware required to allow dash to access management.argotunnel.com requests
|
||||
corsHandler = cors.Handler(cors.Options{
|
||||
// Allows for any subdomain of cloudflare.com
|
||||
AllowedOrigins: []string{"https://*.cloudflare.com"},
|
||||
// Required to present cookies or other authentication across origin boundries
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
})
|
||||
)
|
||||
|
||||
type ManagementService struct {
|
||||
// The management tunnel hostname
|
||||
Hostname string
|
||||
@@ -38,6 +51,9 @@ type ManagementService struct {
|
||||
clientID uuid.UUID
|
||||
label string
|
||||
|
||||
// Additional Handlers
|
||||
metricsHandler http.Handler
|
||||
|
||||
log *zerolog.Logger
|
||||
router chi.Router
|
||||
|
||||
@@ -49,6 +65,7 @@ type ManagementService struct {
|
||||
}
|
||||
|
||||
func New(managementHostname string,
|
||||
enableDiagServices bool,
|
||||
serviceIP string,
|
||||
clientID uuid.UUID,
|
||||
label string,
|
||||
@@ -56,29 +73,31 @@ func New(managementHostname string,
|
||||
logger LoggerListener,
|
||||
) *ManagementService {
|
||||
s := &ManagementService{
|
||||
Hostname: managementHostname,
|
||||
log: log,
|
||||
logger: logger,
|
||||
serviceIP: serviceIP,
|
||||
clientID: clientID,
|
||||
label: label,
|
||||
Hostname: managementHostname,
|
||||
log: log,
|
||||
logger: logger,
|
||||
serviceIP: serviceIP,
|
||||
clientID: clientID,
|
||||
label: label,
|
||||
metricsHandler: promhttp.Handler(),
|
||||
}
|
||||
r := chi.NewRouter()
|
||||
r.Use(ValidateAccessTokenQueryMiddleware)
|
||||
r.Get("/ping", ping)
|
||||
r.Head("/ping", ping)
|
||||
|
||||
// Default management services
|
||||
r.With(corsHandler).Get("/ping", ping)
|
||||
r.With(corsHandler).Head("/ping", ping)
|
||||
r.Get("/logs", s.logs)
|
||||
r.Route("/host_details", func(r chi.Router) {
|
||||
// CORS middleware required to allow dash to access management.argotunnel.com requests
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
// Allows for any subdomain of cloudflare.com
|
||||
AllowedOrigins: []string{"https://*.cloudflare.com"},
|
||||
// Required to present cookies or other authentication across origin boundries
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}))
|
||||
r.Get("/", s.getHostDetails)
|
||||
})
|
||||
r.With(corsHandler).Get("/host_details", s.getHostDetails)
|
||||
|
||||
// Diagnostic management services
|
||||
if enableDiagServices {
|
||||
// Prometheus endpoint
|
||||
r.With(corsHandler).Get("/metrics", s.metricsHandler.ServeHTTP)
|
||||
// Supports only heap and goroutine
|
||||
r.With(corsHandler).Get("/debug/pprof/{profile:heap|goroutine}", pprof.Index)
|
||||
}
|
||||
|
||||
s.router = r
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@ package management
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -15,9 +19,23 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
noopLogger = zerolog.New(io.Discard)
|
||||
noopLogger = zerolog.New(io.Discard)
|
||||
managementHostname = "https://management.argotunnel.com"
|
||||
)
|
||||
|
||||
func TestDisableDiagnosticRoutes(t *testing.T) {
|
||||
mgmt := New("management.argotunnel.com", false, "1.1.1.1:80", uuid.Nil, "", &noopLogger, nil)
|
||||
for _, path := range []string{"/metrics", "/debug/pprof/goroutine", "/debug/pprof/heap"} {
|
||||
t.Run(strings.Replace(path, "/", "_", -1), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", managementHostname+path+"?access_token="+validToken, nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
mgmt.ServeHTTP(recorder, req)
|
||||
resp := recorder.Result()
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadEventsLoop(t *testing.T) {
|
||||
sentEvent := EventStartStreaming{
|
||||
ClientEvent: ClientEvent{Type: StartStreaming},
|
||||
|
||||
@@ -28,8 +28,8 @@ type Orchestrator struct {
|
||||
lock sync.RWMutex
|
||||
// Underlying value is proxy.Proxy, can be read without the lock, but still needs the lock to update
|
||||
proxy atomic.Value
|
||||
// Set of local ingress rules defined at cloudflared startup (separate from user-defined ingress rules)
|
||||
localRules []ingress.Rule
|
||||
// Set of internal ingress rules defined at cloudflared startup (separate from user-defined ingress rules)
|
||||
internalRules []ingress.Rule
|
||||
warpRoutingEnabled atomic.Bool
|
||||
config *Config
|
||||
tags []tunnelpogs.Tag
|
||||
@@ -41,13 +41,13 @@ type Orchestrator struct {
|
||||
proxyShutdownC chan<- struct{}
|
||||
}
|
||||
|
||||
func NewOrchestrator(ctx context.Context, config *Config, tags []tunnelpogs.Tag, localRules []ingress.Rule, log *zerolog.Logger) (*Orchestrator, error) {
|
||||
func NewOrchestrator(ctx context.Context, config *Config, tags []tunnelpogs.Tag, internalRules []ingress.Rule, log *zerolog.Logger) (*Orchestrator, error) {
|
||||
o := &Orchestrator{
|
||||
// Lowest possible version, any remote configuration will have version higher than this
|
||||
// Starting at -1 allows a configuration migration (local to remote) to override the current configuration as it
|
||||
// will start at version 0.
|
||||
currentVersion: -1,
|
||||
localRules: localRules,
|
||||
internalRules: internalRules,
|
||||
config: config,
|
||||
tags: tags,
|
||||
log: log,
|
||||
@@ -116,8 +116,13 @@ func (o *Orchestrator) updateIngress(ingressRules ingress.Ingress, warpRouting i
|
||||
default:
|
||||
}
|
||||
|
||||
// Assign the local ingress rules to the parsed ingress
|
||||
ingressRules.LocalRules = o.localRules
|
||||
// Assign the internal ingress rules to the parsed ingress
|
||||
ingressRules.InternalRules = o.internalRules
|
||||
|
||||
// Check if ingress rules are empty, and add the default route if so.
|
||||
if ingressRules.IsEmpty() {
|
||||
ingressRules.Rules = ingress.GetDefaultIngressRules(o.log)
|
||||
}
|
||||
|
||||
// Start new proxy before closing the ones from last version.
|
||||
// The upside is we don't need to restart proxy from last version, which can fail
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestUpdateConfiguration(t *testing.T) {
|
||||
initConfig := &Config{
|
||||
Ingress: &ingress.Ingress{},
|
||||
}
|
||||
orchestrator, err := NewOrchestrator(context.Background(), initConfig, testTags, []ingress.Rule{ingress.NewManagementRule(management.New("management.argotunnel.com", "1.1.1.1:80", uuid.Nil, "", &testLogger, nil))}, &testLogger)
|
||||
orchestrator, err := NewOrchestrator(context.Background(), initConfig, testTags, []ingress.Rule{ingress.NewManagementRule(management.New("management.argotunnel.com", false, "1.1.1.1:80", uuid.Nil, "", &testLogger, nil))}, &testLogger)
|
||||
require.NoError(t, err)
|
||||
initOriginProxy, err := orchestrator.GetOriginProxy()
|
||||
require.NoError(t, err)
|
||||
@@ -91,15 +91,15 @@ func TestUpdateConfiguration(t *testing.T) {
|
||||
"enabled": true,
|
||||
"connectTimeout": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
updateWithValidation(t, orchestrator, 2, configJSONV2)
|
||||
configV2 := orchestrator.config
|
||||
// Validate local ingress rules
|
||||
require.Equal(t, "management.argotunnel.com", configV2.Ingress.LocalRules[0].Hostname)
|
||||
require.True(t, configV2.Ingress.LocalRules[0].Matches("management.argotunnel.com", "/ping"))
|
||||
require.Equal(t, "management", configV2.Ingress.LocalRules[0].Service.String())
|
||||
// Validate internal ingress rules
|
||||
require.Equal(t, "management.argotunnel.com", configV2.Ingress.InternalRules[0].Hostname)
|
||||
require.True(t, configV2.Ingress.InternalRules[0].Matches("management.argotunnel.com", "/ping"))
|
||||
require.Equal(t, "management", configV2.Ingress.InternalRules[0].Service.String())
|
||||
// Validate ingress rule 0
|
||||
require.Equal(t, "jira.tunnel.org", configV2.Ingress.Rules[0].Hostname)
|
||||
require.True(t, configV2.Ingress.Rules[0].Matches("jira.tunnel.org", "/login"))
|
||||
@@ -145,7 +145,7 @@ func TestUpdateConfiguration(t *testing.T) {
|
||||
{
|
||||
"originRequest":
|
||||
}
|
||||
|
||||
|
||||
`)
|
||||
|
||||
resp = orchestrator.UpdateConfig(3, invalidJSON)
|
||||
@@ -165,7 +165,7 @@ func TestUpdateConfiguration(t *testing.T) {
|
||||
"warp-routing": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
updateWithValidation(t, orchestrator, 10, configJSONV10)
|
||||
configV10 := orchestrator.config
|
||||
@@ -204,12 +204,34 @@ func TestUpdateConfiguration_FromMigration(t *testing.T) {
|
||||
"warp-routing": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
updateWithValidation(t, orchestrator, 0, configJSONV2)
|
||||
require.Len(t, orchestrator.config.Ingress.Rules, 1)
|
||||
}
|
||||
|
||||
// Validates that the default ingress rule will be set if there is no rule provided from the remote.
|
||||
func TestUpdateConfiguration_WithoutIngressRule(t *testing.T) {
|
||||
initConfig := &Config{
|
||||
Ingress: &ingress.Ingress{},
|
||||
}
|
||||
orchestrator, err := NewOrchestrator(context.Background(), initConfig, testTags, []ingress.Rule{}, &testLogger)
|
||||
require.NoError(t, err)
|
||||
initOriginProxy, err := orchestrator.GetOriginProxy()
|
||||
require.NoError(t, err)
|
||||
require.Implements(t, (*connection.OriginProxy)(nil), initOriginProxy)
|
||||
|
||||
// We need to create an empty RemoteConfigJSON because that will get unmarshalled to a RemoteConfig
|
||||
emptyConfig := &ingress.RemoteConfigJSON{}
|
||||
configBytes, err := json.Marshal(emptyConfig)
|
||||
if err != nil {
|
||||
require.FailNow(t, "The RemoteConfigJSON shouldn't fail while being marshalled")
|
||||
}
|
||||
|
||||
updateWithValidation(t, orchestrator, 0, configBytes)
|
||||
require.Len(t, orchestrator.config.Ingress.Rules, 1)
|
||||
}
|
||||
|
||||
// TestConcurrentUpdateAndRead makes sure orchestrator can receive updates and return origin proxy concurrently
|
||||
func TestConcurrentUpdateAndRead(t *testing.T) {
|
||||
const (
|
||||
@@ -428,7 +450,7 @@ func proxyTCP(ctx context.Context, originProxy connection.OriginProxy, originAdd
|
||||
CFRay: "123",
|
||||
LBProbe: false,
|
||||
}
|
||||
rws := connection.NewHTTPResponseReadWriterAcker(respWriter, req)
|
||||
rws := connection.NewHTTPResponseReadWriterAcker(respWriter, w.(http.Flusher), req)
|
||||
|
||||
return originProxy.ProxyTCP(ctx, rws, tcpReq)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,22 @@ var (
|
||||
Help: "Count of error proxying to origin",
|
||||
},
|
||||
)
|
||||
activeTCPSessions = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: connection.MetricsNamespace,
|
||||
Subsystem: "tcp",
|
||||
Name: "active_sessions",
|
||||
Help: "Concurrent count of TCP sessions that are being proxied to any origin",
|
||||
},
|
||||
)
|
||||
totalTCPSessions = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: connection.MetricsNamespace,
|
||||
Subsystem: "tcp",
|
||||
Name: "total_sessions",
|
||||
Help: "Total count of TCP sessions that have been proxied to any origin",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -51,6 +67,8 @@ func init() {
|
||||
concurrentRequests,
|
||||
responseByCode,
|
||||
requestErrors,
|
||||
activeTCPSessions,
|
||||
totalTCPSessions,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,3 +80,14 @@ func incrementRequests() {
|
||||
func decrementConcurrentRequests() {
|
||||
concurrentRequests.Dec()
|
||||
}
|
||||
|
||||
func incrementTCPRequests() {
|
||||
incrementRequests()
|
||||
totalTCPSessions.Inc()
|
||||
activeTCPSessions.Inc()
|
||||
}
|
||||
|
||||
func decrementTCPConcurrentRequests() {
|
||||
decrementConcurrentRequests()
|
||||
activeTCPSessions.Dec()
|
||||
}
|
||||
|
||||
+7
-4
@@ -136,8 +136,11 @@ func (p *Proxy) ProxyHTTP(
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rws := connection.NewHTTPResponseReadWriterAcker(w, req)
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
return fmt.Errorf("response writer is not a flusher")
|
||||
}
|
||||
rws := connection.NewHTTPResponseReadWriterAcker(w, flusher, req)
|
||||
if err := p.proxyStream(tr.ToTracedContext(), rws, dest, originProxy); err != nil {
|
||||
rule, srv := ruleField(p.ingressRules, ruleNum)
|
||||
p.logRequestError(err, cfRay, "", rule, srv)
|
||||
@@ -158,8 +161,8 @@ func (p *Proxy) ProxyTCP(
|
||||
rwa connection.ReadWriteAcker,
|
||||
req *connection.TCPRequest,
|
||||
) error {
|
||||
incrementRequests()
|
||||
defer decrementConcurrentRequests()
|
||||
incrementTCPRequests()
|
||||
defer decrementTCPConcurrentRequests()
|
||||
|
||||
if p.warpRouting == nil {
|
||||
err := errors.New(`cloudflared received a request from WARP client, but your configuration has disabled ingress from WARP clients. To enable this, set "warp-routing:\n\t enabled: true" in your config.yaml`)
|
||||
|
||||
+5
-1
@@ -698,7 +698,7 @@ func TestConnections(t *testing.T) {
|
||||
}()
|
||||
}
|
||||
if test.args.connectionType == connection.TypeTCP {
|
||||
rwa := connection.NewHTTPResponseReadWriterAcker(respWriter, req)
|
||||
rwa := connection.NewHTTPResponseReadWriterAcker(respWriter, respWriter.(http.Flusher), req)
|
||||
err = proxy.ProxyTCP(ctx, rwa, &connection.TCPRequest{Dest: dest})
|
||||
} else {
|
||||
log := zerolog.Nop()
|
||||
@@ -834,6 +834,8 @@ func (w *wsRespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *wsRespWriter) Flush() {}
|
||||
|
||||
func (w *wsRespWriter) AddTrailer(trailerName, trailerValue string) {
|
||||
// do nothing
|
||||
}
|
||||
@@ -873,6 +875,8 @@ func (m *mockTCPRespWriter) Write(p []byte) (n int, err error) {
|
||||
return m.w.Write(p)
|
||||
}
|
||||
|
||||
func (m *mockTCPRespWriter) Flush() {}
|
||||
|
||||
func (m *mockTCPRespWriter) AddTrailer(trailerName, trailerValue string) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
+12
-3
@@ -39,6 +39,9 @@ const (
|
||||
HandshakeIdleTimeout = 5 * time.Second
|
||||
MaxIdleTimeout = 5 * time.Second
|
||||
MaxIdlePingPeriod = 1 * time.Second
|
||||
|
||||
// MaxIncomingStreams is 2^60, which is the maximum supported value by Quic-Go
|
||||
MaxIncomingStreams = 1 << 60
|
||||
)
|
||||
|
||||
// RequestServerStream is a stream to serve requests
|
||||
@@ -226,9 +229,12 @@ func writeSignature(stream io.Writer, signature ProtocolSignature) error {
|
||||
type RPCClientStream struct {
|
||||
client tunnelpogs.CloudflaredServer_PogsClient
|
||||
transport rpc.Transport
|
||||
|
||||
// Time we wait for the server to respond to a request before we close the connection.
|
||||
rpcUnregisterUDPSessionDeadline time.Duration
|
||||
}
|
||||
|
||||
func NewRPCClientStream(ctx context.Context, stream io.ReadWriteCloser, logger *zerolog.Logger) (*RPCClientStream, error) {
|
||||
func NewRPCClientStream(ctx context.Context, stream io.ReadWriteCloser, rpcUnregisterUDPSessionDeadline time.Duration, logger *zerolog.Logger) (*RPCClientStream, error) {
|
||||
n, err := stream.Write(RPCStreamProtocolSignature[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -242,8 +248,9 @@ func NewRPCClientStream(ctx context.Context, stream io.ReadWriteCloser, logger *
|
||||
tunnelrpc.ConnLog(logger),
|
||||
)
|
||||
return &RPCClientStream{
|
||||
client: tunnelpogs.NewCloudflaredServer_PogsClient(conn.Bootstrap(ctx), conn),
|
||||
transport: transport,
|
||||
client: tunnelpogs.NewCloudflaredServer_PogsClient(conn.Bootstrap(ctx), conn),
|
||||
transport: transport,
|
||||
rpcUnregisterUDPSessionDeadline: rpcUnregisterUDPSessionDeadline,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -252,6 +259,8 @@ func (rcs *RPCClientStream) RegisterUdpSession(ctx context.Context, sessionID uu
|
||||
}
|
||||
|
||||
func (rcs *RPCClientStream) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, rcs.rpcUnregisterUDPSessionDeadline)
|
||||
defer cancel()
|
||||
return rcs.client.UnregisterUdpSession(ctx, sessionID, message)
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,63 @@ func TestConnectResponseMeta(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnregisterUdpSession(t *testing.T) {
|
||||
unregisterMessage := "closed by eyeball"
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
sessionRPCServer mockSessionRPCServer
|
||||
timeout time.Duration
|
||||
}{
|
||||
|
||||
{
|
||||
name: "UnregisterUdpSessionTimesout if the RPC server does not respond",
|
||||
sessionRPCServer: mockSessionRPCServer{
|
||||
sessionID: uuid.New(),
|
||||
dstIP: net.IP{172, 16, 0, 1},
|
||||
dstPort: 8000,
|
||||
closeIdleAfter: testCloseIdleAfterHint,
|
||||
unregisterMessage: unregisterMessage,
|
||||
traceContext: "1241ce3ecdefc68854e8514e69ba42ca:b38f1bf5eae406f3:0:1",
|
||||
},
|
||||
// very very low value so we trigger the timeout every time.
|
||||
timeout: time.Nanosecond * 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
logger := zerolog.Nop()
|
||||
clientStream, serverStream := newMockRPCStreams()
|
||||
sessionRegisteredChan := make(chan struct{})
|
||||
go func() {
|
||||
protocol, err := DetermineProtocol(serverStream)
|
||||
assert.NoError(t, err)
|
||||
rpcServerStream, err := NewRPCServerStream(serverStream, protocol)
|
||||
assert.NoError(t, err)
|
||||
err = rpcServerStream.Serve(test.sessionRPCServer, nil, &logger)
|
||||
assert.NoError(t, err)
|
||||
|
||||
serverStream.Close()
|
||||
close(sessionRegisteredChan)
|
||||
}()
|
||||
|
||||
rpcClientStream, err := NewRPCClientStream(context.Background(), clientStream, test.timeout, &logger)
|
||||
assert.NoError(t, err)
|
||||
|
||||
reg, err := rpcClientStream.RegisterUdpSession(context.Background(), test.sessionRPCServer.sessionID, test.sessionRPCServer.dstIP, test.sessionRPCServer.dstPort, testCloseIdleAfterHint, test.sessionRPCServer.traceContext)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, reg.Err)
|
||||
|
||||
assert.Error(t, rpcClientStream.UnregisterUdpSession(context.Background(), test.sessionRPCServer.sessionID, unregisterMessage))
|
||||
|
||||
rpcClientStream.Close()
|
||||
<-sessionRegisteredChan
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRegisterUdpSession(t *testing.T) {
|
||||
unregisterMessage := "closed by eyeball"
|
||||
|
||||
@@ -157,7 +214,7 @@ func TestRegisterUdpSession(t *testing.T) {
|
||||
close(sessionRegisteredChan)
|
||||
}()
|
||||
|
||||
rpcClientStream, err := NewRPCClientStream(context.Background(), clientStream, &logger)
|
||||
rpcClientStream, err := NewRPCClientStream(context.Background(), clientStream, 5*time.Second, &logger)
|
||||
assert.NoError(t, err)
|
||||
|
||||
reg, err := rpcClientStream.RegisterUdpSession(context.Background(), test.sessionRPCServer.sessionID, test.sessionRPCServer.dstIP, test.sessionRPCServer.dstPort, testCloseIdleAfterHint, test.sessionRPCServer.traceContext)
|
||||
@@ -208,7 +265,7 @@ func TestManageConfiguration(t *testing.T) {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
rpcClientStream, err := NewRPCClientStream(ctx, clientStream, &logger)
|
||||
rpcClientStream, err := NewRPCClientStream(ctx, clientStream, 5*time.Second, &logger)
|
||||
assert.NoError(t, err)
|
||||
|
||||
result, err := rpcClientStream.UpdateConfiguration(ctx, version, config)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// DebugStream will tee each read and write to the output logger as a debug message
|
||||
type DebugStream struct {
|
||||
reader io.Reader
|
||||
writer io.Writer
|
||||
log *zerolog.Logger
|
||||
max uint64
|
||||
count atomic.Uint64
|
||||
}
|
||||
|
||||
func NewDebugStream(stream io.ReadWriter, logger *zerolog.Logger, max uint64) *DebugStream {
|
||||
return &DebugStream{
|
||||
reader: stream,
|
||||
writer: stream,
|
||||
log: logger,
|
||||
max: max,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebugStream) Read(p []byte) (n int, err error) {
|
||||
n, err = d.reader.Read(p)
|
||||
if n > 0 && d.max > d.count.Load() {
|
||||
d.count.Add(1)
|
||||
if err != nil {
|
||||
d.log.Err(err).
|
||||
Str("dir", "r").
|
||||
Int("count", n).
|
||||
Msgf("%+q", p[:n])
|
||||
} else {
|
||||
d.log.Debug().
|
||||
Str("dir", "r").
|
||||
Int("count", n).
|
||||
Msgf("%+q", p[:n])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DebugStream) Write(p []byte) (n int, err error) {
|
||||
n, err = d.writer.Write(p)
|
||||
if n > 0 && d.max > d.count.Load() {
|
||||
d.count.Add(1)
|
||||
if err != nil {
|
||||
d.log.Err(err).
|
||||
Str("dir", "w").
|
||||
Int("count", n).
|
||||
Msgf("%+q", p[:n])
|
||||
} else {
|
||||
d.log.Debug().
|
||||
Str("dir", "w").
|
||||
Int("count", n).
|
||||
Msgf("%+q", p[:n])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
+75
-24
@@ -8,13 +8,43 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfio"
|
||||
)
|
||||
|
||||
type Stream interface {
|
||||
Reader
|
||||
WriterCloser
|
||||
}
|
||||
|
||||
type Reader interface {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
type WriterCloser interface {
|
||||
io.Writer
|
||||
WriteCloser
|
||||
}
|
||||
|
||||
type WriteCloser interface {
|
||||
CloseWrite() error
|
||||
}
|
||||
|
||||
type nopCloseWriterAdapter struct {
|
||||
io.ReadWriter
|
||||
}
|
||||
|
||||
func NopCloseWriterAdapter(stream io.ReadWriter) *nopCloseWriterAdapter {
|
||||
return &nopCloseWriterAdapter{stream}
|
||||
}
|
||||
|
||||
func (n *nopCloseWriterAdapter) CloseWrite() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type bidirectionalStreamStatus struct {
|
||||
doneChan chan struct{}
|
||||
anyDone uint32
|
||||
@@ -32,8 +62,24 @@ func (s *bidirectionalStreamStatus) markUniStreamDone() {
|
||||
s.doneChan <- struct{}{}
|
||||
}
|
||||
|
||||
func (s *bidirectionalStreamStatus) waitAnyDone() {
|
||||
func (s *bidirectionalStreamStatus) wait(maxWaitForSecondStream time.Duration) error {
|
||||
<-s.doneChan
|
||||
|
||||
// Only wait for second stream to finish if maxWait is greater than zero
|
||||
if maxWaitForSecondStream > 0 {
|
||||
|
||||
timer := time.NewTimer(maxWaitForSecondStream)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
return fmt.Errorf("timeout waiting for second stream to finish")
|
||||
case <-s.doneChan:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (s *bidirectionalStreamStatus) isAnyDone() bool {
|
||||
return atomic.LoadUint32(&s.anyDone) > 0
|
||||
@@ -41,16 +87,28 @@ func (s *bidirectionalStreamStatus) isAnyDone() bool {
|
||||
|
||||
// Pipe copies copy data to & from provided io.ReadWriters.
|
||||
func Pipe(tunnelConn, originConn io.ReadWriter, log *zerolog.Logger) {
|
||||
status := newBiStreamStatus()
|
||||
|
||||
go unidirectionalStream(tunnelConn, originConn, "origin->tunnel", status, log)
|
||||
go unidirectionalStream(originConn, tunnelConn, "tunnel->origin", status, log)
|
||||
|
||||
// If one side is done, we are done.
|
||||
status.waitAnyDone()
|
||||
PipeBidirectional(NopCloseWriterAdapter(tunnelConn), NopCloseWriterAdapter(originConn), 0, log)
|
||||
}
|
||||
|
||||
func unidirectionalStream(dst io.Writer, src io.Reader, dir string, status *bidirectionalStreamStatus, log *zerolog.Logger) {
|
||||
// PipeBidirectional copies data two BidirectionStreams. It is a special case of Pipe where it receives a concept that allows for Read and Write side to be closed independently.
|
||||
// The main difference is that when piping data from a reader to a writer, if EOF is read, then this implementation propagates the EOF signal to the destination/writer by closing the write side of the
|
||||
// Bidirectional Stream.
|
||||
// Finally, depending on once EOF is ready from one of the provided streams, the other direction of streaming data will have a configured time period to also finish, otherwise,
|
||||
// the method will return immediately with a timeout error. It is however, the responsability of the caller to close the associated streams in both ends in order to free all the resources/go-routines.
|
||||
func PipeBidirectional(downstream, upstream Stream, maxWaitForSecondStream time.Duration, log *zerolog.Logger) error {
|
||||
status := newBiStreamStatus()
|
||||
|
||||
go unidirectionalStream(downstream, upstream, "upstream->downstream", status, log)
|
||||
go unidirectionalStream(upstream, downstream, "downstream->upstream", status, log)
|
||||
|
||||
if err := status.wait(maxWaitForSecondStream); err != nil {
|
||||
return errors.Wrap(err, "unable to wait for both streams while proxying")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unidirectionalStream(dst WriterCloser, src Reader, dir string, status *bidirectionalStreamStatus, log *zerolog.Logger) {
|
||||
defer func() {
|
||||
// The bidirectional streaming spawns 2 goroutines to stream each direction.
|
||||
// If any ends, the callstack returns, meaning the Tunnel request/stream (depending on http2 vs quic) will
|
||||
@@ -58,28 +116,21 @@ func unidirectionalStream(dst io.Writer, src io.Reader, dir string, status *bidi
|
||||
// server/origin listens forever until closure), it may read/write from the underlying ReadWriter (backed by
|
||||
// the Edge<->cloudflared transport) in an unexpected state.
|
||||
// Because of this, we set this recover() logic.
|
||||
if r := recover(); r != nil {
|
||||
if err := recover(); err != nil {
|
||||
if status.isAnyDone() {
|
||||
// We handle such unexpected errors only when we detect that one side of the streaming is done.
|
||||
log.Debug().Msgf("Gracefully handled error %v in Streaming for %s, error %s", r, dir, debug.Stack())
|
||||
log.Debug().Msgf("recovered from panic in stream.Pipe for %s, error %s, %s", dir, err, debug.Stack())
|
||||
} else {
|
||||
// Otherwise, this is unexpected, but we prevent the program from crashing anyway.
|
||||
log.Warn().Msgf("Gracefully handled unexpected error %v in Streaming for %s, error %s", r, dir, debug.Stack())
|
||||
|
||||
tags := make(map[string]string)
|
||||
tags["root"] = "websocket.stream"
|
||||
tags["dir"] = dir
|
||||
switch rval := r.(type) {
|
||||
case error:
|
||||
raven.CaptureError(rval, tags)
|
||||
default:
|
||||
rvalStr := fmt.Sprint(rval)
|
||||
raven.CaptureMessage(rvalStr, tags)
|
||||
}
|
||||
log.Warn().Msgf("recovered from panic in stream.Pipe for %s, error %s, %s", dir, err, debug.Stack())
|
||||
sentry.CurrentHub().Recover(err)
|
||||
sentry.Flush(time.Second * 5)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
defer dst.CloseWrite()
|
||||
|
||||
_, err := copyData(dst, src, dir)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("%s copy: %v", dir, err)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPipeBidirectionalFinishBothSides(t *testing.T) {
|
||||
fun := func(upstream, downstream *mockedStream) {
|
||||
downstream.closeReader()
|
||||
upstream.closeReader()
|
||||
}
|
||||
|
||||
testPipeBidirectionalUnblocking(t, fun, time.Millisecond*200, false)
|
||||
}
|
||||
|
||||
func TestPipeBidirectionalFinishOneSideTimeout(t *testing.T) {
|
||||
fun := func(upstream, downstream *mockedStream) {
|
||||
downstream.closeReader()
|
||||
}
|
||||
|
||||
testPipeBidirectionalUnblocking(t, fun, time.Millisecond*200, true)
|
||||
}
|
||||
|
||||
func TestPipeBidirectionalClosingWriteBothSidesAlsoExists(t *testing.T) {
|
||||
fun := func(upstream, downstream *mockedStream) {
|
||||
downstream.CloseWrite()
|
||||
upstream.CloseWrite()
|
||||
|
||||
downstream.writeToReader("abc")
|
||||
upstream.writeToReader("abc")
|
||||
}
|
||||
|
||||
testPipeBidirectionalUnblocking(t, fun, time.Millisecond*200, false)
|
||||
}
|
||||
|
||||
func TestPipeBidirectionalClosingWriteSingleSideAlsoExists(t *testing.T) {
|
||||
fun := func(upstream, downstream *mockedStream) {
|
||||
downstream.CloseWrite()
|
||||
|
||||
downstream.writeToReader("abc")
|
||||
upstream.writeToReader("abc")
|
||||
}
|
||||
|
||||
testPipeBidirectionalUnblocking(t, fun, time.Millisecond*200, true)
|
||||
}
|
||||
|
||||
func testPipeBidirectionalUnblocking(t *testing.T, afterFun func(*mockedStream, *mockedStream), timeout time.Duration, expectTimeout bool) {
|
||||
logger := zerolog.Nop()
|
||||
|
||||
downstream := newMockedStream()
|
||||
upstream := newMockedStream()
|
||||
|
||||
resultCh := make(chan error)
|
||||
go func() {
|
||||
resultCh <- PipeBidirectional(downstream, upstream, timeout, &logger)
|
||||
}()
|
||||
|
||||
afterFun(upstream, downstream)
|
||||
|
||||
select {
|
||||
case err := <-resultCh:
|
||||
if expectTimeout {
|
||||
require.NotNil(t, err)
|
||||
} else {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
case <-time.After(timeout * 2):
|
||||
require.Fail(t, "test timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func newMockedStream() *mockedStream {
|
||||
return &mockedStream{
|
||||
readCh: make(chan *string),
|
||||
writeCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
type mockedStream struct {
|
||||
readCh chan *string
|
||||
writeCh chan struct{}
|
||||
|
||||
writeCloseOnce sync.Once
|
||||
}
|
||||
|
||||
func (m *mockedStream) Read(p []byte) (n int, err error) {
|
||||
result := <-m.readCh
|
||||
if result == nil {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
return len(*result), nil
|
||||
}
|
||||
|
||||
func (m *mockedStream) Write(p []byte) (n int, err error) {
|
||||
<-m.writeCh
|
||||
|
||||
return 0, fmt.Errorf("closed")
|
||||
}
|
||||
|
||||
func (m *mockedStream) CloseWrite() error {
|
||||
m.writeCloseOnce.Do(func() {
|
||||
close(m.writeCh)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedStream) closeReader() {
|
||||
close(m.readCh)
|
||||
}
|
||||
func (m *mockedStream) writeToReader(content string) {
|
||||
m.readCh <- &content
|
||||
}
|
||||
+16
-9
@@ -68,6 +68,10 @@ type TunnelConfig struct {
|
||||
ProtocolSelector connection.ProtocolSelector
|
||||
EdgeTLSConfigs map[connection.Protocol]*tls.Config
|
||||
PacketConfig *ingress.GlobalRouterConfig
|
||||
|
||||
UDPUnregisterSessionTimeout time.Duration
|
||||
|
||||
DisableQUICPathMTUDiscovery bool
|
||||
}
|
||||
|
||||
func (c *TunnelConfig) registrationOptions(connectionID uint8, OriginLocalIP string, uuid uuid.UUID) *tunnelpogs.RegistrationOptions {
|
||||
@@ -594,14 +598,15 @@ func (e *EdgeTunnelServer) serveQUIC(
|
||||
}
|
||||
|
||||
quicConfig := &quic.Config{
|
||||
HandshakeIdleTimeout: quicpogs.HandshakeIdleTimeout,
|
||||
MaxIdleTimeout: quicpogs.MaxIdleTimeout,
|
||||
KeepAlivePeriod: quicpogs.MaxIdlePingPeriod,
|
||||
MaxIncomingStreams: connection.MaxConcurrentStreams,
|
||||
MaxIncomingUniStreams: connection.MaxConcurrentStreams,
|
||||
EnableDatagrams: true,
|
||||
MaxDatagramFrameSize: quicpogs.MaxDatagramFrameSize,
|
||||
Tracer: quicpogs.NewClientTracer(connLogger.Logger(), connIndex),
|
||||
HandshakeIdleTimeout: quicpogs.HandshakeIdleTimeout,
|
||||
MaxIdleTimeout: quicpogs.MaxIdleTimeout,
|
||||
KeepAlivePeriod: quicpogs.MaxIdlePingPeriod,
|
||||
MaxIncomingStreams: quicpogs.MaxIncomingStreams,
|
||||
MaxIncomingUniStreams: quicpogs.MaxIncomingStreams,
|
||||
EnableDatagrams: true,
|
||||
MaxDatagramFrameSize: quicpogs.MaxDatagramFrameSize,
|
||||
Tracer: quicpogs.NewClientTracer(connLogger.Logger(), connIndex),
|
||||
DisablePathMTUDiscovery: e.config.DisableQUICPathMTUDiscovery,
|
||||
}
|
||||
|
||||
quicConn, err := connection.NewQUICConnection(
|
||||
@@ -615,7 +620,9 @@ func (e *EdgeTunnelServer) serveQUIC(
|
||||
connOptions,
|
||||
controlStreamHandler,
|
||||
connLogger.Logger(),
|
||||
e.config.PacketConfig)
|
||||
e.config.PacketConfig,
|
||||
e.config.UDPUnregisterSessionTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
if e.config.NeedPQ {
|
||||
handlePQTunnelError(err, e.config)
|
||||
|
||||
-373
@@ -1,373 +0,0 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
# GoCertifi: SSL Certificates for Golang
|
||||
|
||||
This Go package contains a CA bundle that you can reference in your Go code.
|
||||
This is useful for systems that do not have CA bundles that Golang can find
|
||||
itself, or where a uniform set of CAs is valuable.
|
||||
|
||||
This is the same CA bundle that ships with the
|
||||
[Python Requests](https://github.com/kennethreitz/requests) library, and is a
|
||||
Golang specific port of [certifi](https://github.com/kennethreitz/certifi). The
|
||||
CA bundle is derived from Mozilla's canonical set.
|
||||
|
||||
## Usage
|
||||
|
||||
You can use the `gocertifi` package as follows:
|
||||
|
||||
```go
|
||||
import "github.com/certifi/gocertifi"
|
||||
|
||||
certPool, err := gocertifi.CACerts()
|
||||
```
|
||||
|
||||
You can use the returned `*x509.CertPool` as part of an HTTP transport, for example:
|
||||
|
||||
```go
|
||||
import (
|
||||
"net/http"
|
||||
"crypto/tls"
|
||||
)
|
||||
|
||||
// Setup an HTTP client with a custom transport
|
||||
transport := &http.Transport{
|
||||
Proxy: ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
// or, starting with go1.13 simply use:
|
||||
// transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
transport.TLSClientConfig = &tls.Config{RootCAs: certPool}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
// Make an HTTP request using our custom transport
|
||||
resp, err := client.Get("https://example.com")
|
||||
```
|
||||
|
||||
## Detailed Documentation
|
||||
|
||||
Import as follows:
|
||||
|
||||
```go
|
||||
import "github.com/certifi/gocertifi"
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
```go
|
||||
func CACerts() (*x509.CertPool, error)
|
||||
```
|
||||
CACerts builds an X.509 certificate pool containing the Mozilla CA Certificate
|
||||
bundle. This can't actually error and always returns successfully with `nil`
|
||||
as the error. This will be replaced in `v2` to only return the `CertPool`.
|
||||
-3219
File diff suppressed because it is too large
Load Diff
-1
@@ -1 +0,0 @@
|
||||
.git
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
*.test
|
||||
*.out
|
||||
example/example
|
||||
/xunit.xml
|
||||
/coverage.xml
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- tip
|
||||
|
||||
before_install:
|
||||
- go install -race std
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
- go get github.com/tebeka/go2xunit
|
||||
- go get github.com/t-yuki/gocover-cobertura
|
||||
- go get -v ./...
|
||||
|
||||
script:
|
||||
- go test -v -race ./... | tee gotest.out
|
||||
- $GOPATH/bin/go2xunit -fail -input gotest.out -output xunit.xml
|
||||
- go test -v -coverprofile=coverage.txt -covermode count .
|
||||
- $GOPATH/bin/gocover-cobertura < coverage.txt > coverage.xml
|
||||
|
||||
after_script:
|
||||
- npm install -g @zeus-ci/cli
|
||||
- zeus upload -t "application/x-cobertura+xml" coverage.xml
|
||||
- zeus upload -t "application/x-xunit+xml" xunit.xml
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://zeus.ci/hooks/cd949996-d30a-11e8-ba53-0a580a28042d/public/provider/travis/webhook
|
||||
on_success: always
|
||||
on_failure: always
|
||||
on_start: always
|
||||
on_cancel: always
|
||||
on_error: always
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
Copyright (c) 2013 Apollic Software, LLC. All rights reserved.
|
||||
Copyright (c) 2015 Functional Software, Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Apollic Software, LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
# raven
|
||||
|
||||
[](https://travis-ci.org/getsentry/raven-go)
|
||||
[](https://goreportcard.com/report/github.com/getsentry/raven-go)
|
||||
[](https://godoc.org/github.com/getsentry/raven-go)
|
||||
|
||||
raven is the official Go SDK for the [Sentry](https://github.com/getsentry/sentry)
|
||||
event/error logging system.
|
||||
|
||||
- [**API Documentation**](https://godoc.org/github.com/getsentry/raven-go)
|
||||
- [**Usage and Examples**](https://docs.sentry.io/clients/go/)
|
||||
|
||||
## Installation
|
||||
|
||||
```text
|
||||
go get github.com/getsentry/raven-go
|
||||
```
|
||||
|
||||
Note: Go 1.7 and newer are supported.
|
||||
-977
@@ -1,977 +0,0 @@
|
||||
// Package raven implements a client for the Sentry error logging service.
|
||||
package raven
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
mrand "math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/certifi/gocertifi"
|
||||
pkgErrors "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
userAgent = "raven-go/1.0"
|
||||
timestampFormat = `"2006-01-02T15:04:05.00"`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPacketDropped = errors.New("raven: packet dropped")
|
||||
ErrUnableToUnmarshalJSON = errors.New("raven: unable to unmarshal JSON")
|
||||
ErrMissingUser = errors.New("raven: dsn missing public key and/or password")
|
||||
ErrMissingProjectID = errors.New("raven: dsn missing project id")
|
||||
ErrInvalidSampleRate = errors.New("raven: sample rate should be between 0 and 1")
|
||||
)
|
||||
|
||||
type Severity string
|
||||
|
||||
// http://docs.python.org/2/howto/logging.html#logging-levels
|
||||
const (
|
||||
DEBUG = Severity("debug")
|
||||
INFO = Severity("info")
|
||||
WARNING = Severity("warning")
|
||||
ERROR = Severity("error")
|
||||
FATAL = Severity("fatal")
|
||||
)
|
||||
|
||||
type Timestamp time.Time
|
||||
|
||||
func (t Timestamp) MarshalJSON() ([]byte, error) {
|
||||
return []byte(time.Time(t).UTC().Format(timestampFormat)), nil
|
||||
}
|
||||
|
||||
func (timestamp *Timestamp) UnmarshalJSON(data []byte) error {
|
||||
t, err := time.Parse(timestampFormat, string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*timestamp = Timestamp(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (timestamp Timestamp) Format(format string) string {
|
||||
t := time.Time(timestamp)
|
||||
return t.Format(format)
|
||||
}
|
||||
|
||||
// An Interface is a Sentry interface that will be serialized as JSON.
|
||||
// It must implement json.Marshaler or use json struct tags.
|
||||
type Interface interface {
|
||||
// The Sentry class name. Example: sentry.interfaces.Stacktrace
|
||||
Class() string
|
||||
}
|
||||
|
||||
type Culpriter interface {
|
||||
Culprit() string
|
||||
}
|
||||
|
||||
type Transport interface {
|
||||
Send(url, authHeader string, packet *Packet) error
|
||||
}
|
||||
|
||||
type Extra map[string]interface{}
|
||||
|
||||
type outgoingPacket struct {
|
||||
packet *Packet
|
||||
ch chan error
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type Tags []Tag
|
||||
|
||||
func (tag *Tag) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal([2]string{tag.Key, tag.Value})
|
||||
}
|
||||
|
||||
func (t *Tag) UnmarshalJSON(data []byte) error {
|
||||
var tag [2]string
|
||||
if err := json.Unmarshal(data, &tag); err != nil {
|
||||
return err
|
||||
}
|
||||
*t = Tag{tag[0], tag[1]}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tags) UnmarshalJSON(data []byte) error {
|
||||
var tags []Tag
|
||||
|
||||
switch data[0] {
|
||||
case '[':
|
||||
// Unmarshal into []Tag
|
||||
if err := json.Unmarshal(data, &tags); err != nil {
|
||||
return err
|
||||
}
|
||||
case '{':
|
||||
// Unmarshal into map[string]string
|
||||
tagMap := make(map[string]string)
|
||||
if err := json.Unmarshal(data, &tagMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert to []Tag
|
||||
for k, v := range tagMap {
|
||||
tags = append(tags, Tag{k, v})
|
||||
}
|
||||
default:
|
||||
return ErrUnableToUnmarshalJSON
|
||||
}
|
||||
|
||||
*t = tags
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/#building-the-json-packet
|
||||
type Packet struct {
|
||||
// Required
|
||||
Message string `json:"message"`
|
||||
|
||||
// Required, set automatically by Client.Send/Report via Packet.Init if blank
|
||||
EventID string `json:"event_id"`
|
||||
Project string `json:"project"`
|
||||
Timestamp Timestamp `json:"timestamp"`
|
||||
Level Severity `json:"level"`
|
||||
Logger string `json:"logger"`
|
||||
|
||||
// Optional
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Culprit string `json:"culprit,omitempty"`
|
||||
ServerName string `json:"server_name,omitempty"`
|
||||
Release string `json:"release,omitempty"`
|
||||
Environment string `json:"environment,omitempty"`
|
||||
Tags Tags `json:"tags,omitempty"`
|
||||
Modules map[string]string `json:"modules,omitempty"`
|
||||
Fingerprint []string `json:"fingerprint,omitempty"`
|
||||
Extra Extra `json:"extra,omitempty"`
|
||||
|
||||
Interfaces []Interface `json:"-"`
|
||||
}
|
||||
|
||||
// NewPacket constructs a packet with the specified message and interfaces.
|
||||
func NewPacket(message string, interfaces ...Interface) *Packet {
|
||||
extra := Extra{}
|
||||
setExtraDefaults(extra)
|
||||
return &Packet{
|
||||
Message: message,
|
||||
Interfaces: interfaces,
|
||||
Extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPacketWithExtra constructs a packet with the specified message, extra information, and interfaces.
|
||||
func NewPacketWithExtra(message string, extra Extra, interfaces ...Interface) *Packet {
|
||||
if extra == nil {
|
||||
extra = Extra{}
|
||||
}
|
||||
setExtraDefaults(extra)
|
||||
|
||||
return &Packet{
|
||||
Message: message,
|
||||
Interfaces: interfaces,
|
||||
Extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
func setExtraDefaults(extra Extra) Extra {
|
||||
extra["runtime.Version"] = runtime.Version()
|
||||
extra["runtime.NumCPU"] = runtime.NumCPU()
|
||||
extra["runtime.GOMAXPROCS"] = runtime.GOMAXPROCS(0) // 0 just returns the current value
|
||||
extra["runtime.NumGoroutine"] = runtime.NumGoroutine()
|
||||
return extra
|
||||
}
|
||||
|
||||
// Init initializes required fields in a packet. It is typically called by
|
||||
// Client.Send/Report automatically.
|
||||
func (packet *Packet) Init(project string) error {
|
||||
if packet.Project == "" {
|
||||
packet.Project = project
|
||||
}
|
||||
if packet.EventID == "" {
|
||||
var err error
|
||||
packet.EventID, err = uuid()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if time.Time(packet.Timestamp).IsZero() {
|
||||
packet.Timestamp = Timestamp(time.Now())
|
||||
}
|
||||
if packet.Level == "" {
|
||||
packet.Level = ERROR
|
||||
}
|
||||
if packet.Logger == "" {
|
||||
packet.Logger = "root"
|
||||
}
|
||||
if packet.ServerName == "" {
|
||||
packet.ServerName = hostname
|
||||
}
|
||||
if packet.Platform == "" {
|
||||
packet.Platform = "go"
|
||||
}
|
||||
|
||||
if packet.Culprit == "" {
|
||||
for _, inter := range packet.Interfaces {
|
||||
if c, ok := inter.(Culpriter); ok {
|
||||
packet.Culprit = c.Culprit()
|
||||
if packet.Culprit != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (packet *Packet) AddTags(tags map[string]string) {
|
||||
for k, v := range tags {
|
||||
packet.Tags = append(packet.Tags, Tag{k, v})
|
||||
}
|
||||
}
|
||||
|
||||
func uuid() (string, error) {
|
||||
id := make([]byte, 16)
|
||||
_, err := io.ReadFull(rand.Reader, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id[6] &= 0x0F // clear version
|
||||
id[6] |= 0x40 // set version to 4 (random uuid)
|
||||
id[8] &= 0x3F // clear variant
|
||||
id[8] |= 0x80 // set to IETF variant
|
||||
return hex.EncodeToString(id), nil
|
||||
}
|
||||
|
||||
func (packet *Packet) JSON() ([]byte, error) {
|
||||
packetJSON, err := json.Marshal(packet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
interfaces := make(map[string]Interface, len(packet.Interfaces))
|
||||
for _, inter := range packet.Interfaces {
|
||||
if inter != nil {
|
||||
interfaces[inter.Class()] = inter
|
||||
}
|
||||
}
|
||||
|
||||
if len(interfaces) > 0 {
|
||||
interfaceJSON, err := json.Marshal(interfaces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packetJSON[len(packetJSON)-1] = ','
|
||||
packetJSON = append(packetJSON, interfaceJSON[1:]...)
|
||||
}
|
||||
|
||||
return packetJSON, nil
|
||||
}
|
||||
|
||||
type context struct {
|
||||
user *User
|
||||
http *Http
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
func (c *context) setUser(u *User) { c.user = u }
|
||||
func (c *context) setHttp(h *Http) { c.http = h }
|
||||
func (c *context) setTags(t map[string]string) {
|
||||
if c.tags == nil {
|
||||
c.tags = make(map[string]string)
|
||||
}
|
||||
for k, v := range t {
|
||||
c.tags[k] = v
|
||||
}
|
||||
}
|
||||
func (c *context) clear() {
|
||||
c.user = nil
|
||||
c.http = nil
|
||||
c.tags = nil
|
||||
}
|
||||
|
||||
// Return a list of interfaces to be used in appending with the rest
|
||||
func (c *context) interfaces() []Interface {
|
||||
len, i := 0, 0
|
||||
if c.user != nil {
|
||||
len++
|
||||
}
|
||||
if c.http != nil {
|
||||
len++
|
||||
}
|
||||
interfaces := make([]Interface, len)
|
||||
if c.user != nil {
|
||||
interfaces[i] = c.user
|
||||
i++
|
||||
}
|
||||
if c.http != nil {
|
||||
interfaces[i] = c.http
|
||||
i++
|
||||
}
|
||||
return interfaces
|
||||
}
|
||||
|
||||
// The maximum number of packets that will be buffered waiting to be delivered.
|
||||
// Packets will be dropped if the buffer is full. Used by NewClient.
|
||||
var MaxQueueBuffer = 100
|
||||
|
||||
func newTransport() Transport {
|
||||
t := &HTTPTransport{}
|
||||
rootCAs, err := gocertifi.CACerts()
|
||||
if err != nil {
|
||||
log.Println("raven: failed to load root TLS certificates:", err)
|
||||
} else {
|
||||
t.Client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{RootCAs: rootCAs},
|
||||
},
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func newClient(tags map[string]string) *Client {
|
||||
client := &Client{
|
||||
Transport: newTransport(),
|
||||
Tags: tags,
|
||||
context: &context{},
|
||||
sampleRate: 1.0,
|
||||
queue: make(chan *outgoingPacket, MaxQueueBuffer),
|
||||
}
|
||||
client.SetDSN(os.Getenv("SENTRY_DSN"))
|
||||
client.SetRelease(os.Getenv("SENTRY_RELEASE"))
|
||||
client.SetEnvironment(os.Getenv("SENTRY_ENVIRONMENT"))
|
||||
return client
|
||||
}
|
||||
|
||||
// New constructs a new Sentry client instance
|
||||
func New(dsn string) (*Client, error) {
|
||||
client := newClient(nil)
|
||||
return client, client.SetDSN(dsn)
|
||||
}
|
||||
|
||||
// NewWithTags constructs a new Sentry client instance with default tags.
|
||||
func NewWithTags(dsn string, tags map[string]string) (*Client, error) {
|
||||
client := newClient(tags)
|
||||
return client, client.SetDSN(dsn)
|
||||
}
|
||||
|
||||
// NewClient constructs a Sentry client and spawns a background goroutine to
|
||||
// handle packets sent by Client.Report.
|
||||
//
|
||||
// Deprecated: use New and NewWithTags instead
|
||||
func NewClient(dsn string, tags map[string]string) (*Client, error) {
|
||||
client := newClient(tags)
|
||||
return client, client.SetDSN(dsn)
|
||||
}
|
||||
|
||||
// Client encapsulates a connection to a Sentry server. It must be initialized
|
||||
// by calling NewClient. Modification of fields concurrently with Send or after
|
||||
// calling Report for the first time is not thread-safe.
|
||||
type Client struct {
|
||||
Tags map[string]string
|
||||
|
||||
Transport Transport
|
||||
|
||||
// DropHandler is called when a packet is dropped because the buffer is full.
|
||||
DropHandler func(*Packet)
|
||||
|
||||
// Context that will get appending to all packets
|
||||
context *context
|
||||
|
||||
mu sync.RWMutex
|
||||
url string
|
||||
projectID string
|
||||
authHeader string
|
||||
release string
|
||||
environment string
|
||||
sampleRate float32
|
||||
|
||||
// default logger name (leave empty for 'root')
|
||||
defaultLoggerName string
|
||||
|
||||
includePaths []string
|
||||
ignoreErrorsRegexp *regexp.Regexp
|
||||
queue chan *outgoingPacket
|
||||
|
||||
// A WaitGroup to keep track of all currently in-progress captures
|
||||
// This is intended to be used with Client.Wait() to assure that
|
||||
// all messages have been transported before exiting the process.
|
||||
wg sync.WaitGroup
|
||||
|
||||
// A Once to track only starting up the background worker once
|
||||
start sync.Once
|
||||
}
|
||||
|
||||
// Initialize a default *Client instance
|
||||
var DefaultClient = newClient(nil)
|
||||
|
||||
func (c *Client) SetIgnoreErrors(errs []string) error {
|
||||
joinedRegexp := strings.Join(errs, "|")
|
||||
r, err := regexp.Compile(joinedRegexp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile regexp %q for %q: %v", joinedRegexp, errs, err)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.ignoreErrorsRegexp = r
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) shouldExcludeErr(errStr string) bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.ignoreErrorsRegexp != nil && c.ignoreErrorsRegexp.MatchString(errStr)
|
||||
}
|
||||
|
||||
func SetIgnoreErrors(errs ...string) error {
|
||||
return DefaultClient.SetIgnoreErrors(errs)
|
||||
}
|
||||
|
||||
// SetDSN updates a client with a new DSN. It safe to call after and
|
||||
// concurrently with calls to Report and Send.
|
||||
func (client *Client) SetDSN(dsn string) error {
|
||||
if dsn == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
|
||||
uri, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if uri.User == nil {
|
||||
return ErrMissingUser
|
||||
}
|
||||
publicKey := uri.User.Username()
|
||||
secretKey, hasSecretKey := uri.User.Password()
|
||||
uri.User = nil
|
||||
|
||||
if idx := strings.LastIndex(uri.Path, "/"); idx != -1 {
|
||||
client.projectID = uri.Path[idx+1:]
|
||||
uri.Path = uri.Path[:idx+1] + "api/" + client.projectID + "/store/"
|
||||
}
|
||||
if client.projectID == "" {
|
||||
return ErrMissingProjectID
|
||||
}
|
||||
|
||||
client.url = uri.String()
|
||||
|
||||
if hasSecretKey {
|
||||
client.authHeader = fmt.Sprintf("Sentry sentry_version=4, sentry_key=%s, sentry_secret=%s", publicKey, secretKey)
|
||||
} else {
|
||||
client.authHeader = fmt.Sprintf("Sentry sentry_version=4, sentry_key=%s", publicKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sets the DSN for the default *Client instance
|
||||
func SetDSN(dsn string) error { return DefaultClient.SetDSN(dsn) }
|
||||
|
||||
// SetRelease sets the "release" tag.
|
||||
func (client *Client) SetRelease(release string) {
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
client.release = release
|
||||
}
|
||||
|
||||
// SetEnvironment sets the "environment" tag.
|
||||
func (client *Client) SetEnvironment(environment string) {
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
client.environment = environment
|
||||
}
|
||||
|
||||
// SetDefaultLoggerName sets the default logger name.
|
||||
func (client *Client) SetDefaultLoggerName(name string) {
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
client.defaultLoggerName = name
|
||||
}
|
||||
|
||||
// SetSampleRate sets how much sampling we want on client side
|
||||
func (client *Client) SetSampleRate(rate float32) error {
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
|
||||
if rate < 0 || rate > 1 {
|
||||
return ErrInvalidSampleRate
|
||||
}
|
||||
client.sampleRate = rate
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetRelease sets the "release" tag on the default *Client
|
||||
func SetRelease(release string) { DefaultClient.SetRelease(release) }
|
||||
|
||||
// SetEnvironment sets the "environment" tag on the default *Client
|
||||
func SetEnvironment(environment string) { DefaultClient.SetEnvironment(environment) }
|
||||
|
||||
// SetDefaultLoggerName sets the "defaultLoggerName" on the default *Client
|
||||
func SetDefaultLoggerName(name string) {
|
||||
DefaultClient.SetDefaultLoggerName(name)
|
||||
}
|
||||
|
||||
// SetSampleRate sets the "sample rate" on the degault *Client
|
||||
func SetSampleRate(rate float32) error { return DefaultClient.SetSampleRate(rate) }
|
||||
|
||||
func (client *Client) worker() {
|
||||
for outgoingPacket := range client.queue {
|
||||
|
||||
client.mu.RLock()
|
||||
url, authHeader := client.url, client.authHeader
|
||||
client.mu.RUnlock()
|
||||
|
||||
outgoingPacket.ch <- client.Transport.Send(url, authHeader, outgoingPacket.packet)
|
||||
client.wg.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// Capture asynchronously delivers a packet to the Sentry server. It is a no-op
|
||||
// when client is nil. A channel is provided if it is important to check for a
|
||||
// send's success.
|
||||
func (client *Client) Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) {
|
||||
ch = make(chan error, 1)
|
||||
|
||||
if client == nil {
|
||||
// return a chan that always returns nil when the caller receives from it
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
|
||||
if client.sampleRate < 1.0 && mrand.Float32() > client.sampleRate {
|
||||
return
|
||||
}
|
||||
|
||||
if packet == nil {
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
|
||||
if client.shouldExcludeErr(packet.Message) {
|
||||
return
|
||||
}
|
||||
|
||||
// Keep track of all running Captures so that we can wait for them all to finish
|
||||
// *Must* call client.wg.Done() on any path that indicates that an event was
|
||||
// finished being acted upon, whether success or failure
|
||||
client.wg.Add(1)
|
||||
|
||||
// Merge capture tags and client tags
|
||||
packet.AddTags(captureTags)
|
||||
packet.AddTags(client.Tags)
|
||||
|
||||
// Initialize any required packet fields
|
||||
client.mu.RLock()
|
||||
packet.AddTags(client.context.tags)
|
||||
projectID := client.projectID
|
||||
release := client.release
|
||||
environment := client.environment
|
||||
defaultLoggerName := client.defaultLoggerName
|
||||
client.mu.RUnlock()
|
||||
|
||||
// set the global logger name on the packet if we must
|
||||
if packet.Logger == "" && defaultLoggerName != "" {
|
||||
packet.Logger = defaultLoggerName
|
||||
}
|
||||
|
||||
err := packet.Init(projectID)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
client.wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
if packet.Release == "" {
|
||||
packet.Release = release
|
||||
}
|
||||
|
||||
if packet.Environment == "" {
|
||||
packet.Environment = environment
|
||||
}
|
||||
|
||||
outgoingPacket := &outgoingPacket{packet, ch}
|
||||
|
||||
// Lazily start background worker until we
|
||||
// do our first write into the queue.
|
||||
client.start.Do(func() {
|
||||
go client.worker()
|
||||
})
|
||||
|
||||
select {
|
||||
case client.queue <- outgoingPacket:
|
||||
default:
|
||||
// Send would block, drop the packet
|
||||
if client.DropHandler != nil {
|
||||
client.DropHandler(packet)
|
||||
}
|
||||
ch <- ErrPacketDropped
|
||||
client.wg.Done()
|
||||
}
|
||||
|
||||
return packet.EventID, ch
|
||||
}
|
||||
|
||||
// Capture asynchronously delivers a packet to the Sentry server with the default *Client.
|
||||
// It is a no-op when client is nil. A channel is provided if it is important to check for a
|
||||
// send's success.
|
||||
func Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) {
|
||||
return DefaultClient.Capture(packet, captureTags)
|
||||
}
|
||||
|
||||
// CaptureMessage formats and delivers a string message to the Sentry server.
|
||||
func (client *Client) CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string {
|
||||
if client == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if client.shouldExcludeErr(message) {
|
||||
return ""
|
||||
}
|
||||
|
||||
packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...)
|
||||
eventID, _ := client.Capture(packet, tags)
|
||||
|
||||
return eventID
|
||||
}
|
||||
|
||||
// CaptureMessage formats and delivers a string message to the Sentry server with the default *Client
|
||||
func CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string {
|
||||
return DefaultClient.CaptureMessage(message, tags, interfaces...)
|
||||
}
|
||||
|
||||
// CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent.
|
||||
func (client *Client) CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string {
|
||||
if client == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if client.shouldExcludeErr(message) {
|
||||
return ""
|
||||
}
|
||||
|
||||
packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...)
|
||||
eventID, ch := client.Capture(packet, tags)
|
||||
if eventID != "" {
|
||||
<-ch
|
||||
}
|
||||
|
||||
return eventID
|
||||
}
|
||||
|
||||
// CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent.
|
||||
func CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string {
|
||||
return DefaultClient.CaptureMessageAndWait(message, tags, interfaces...)
|
||||
}
|
||||
|
||||
// CaptureErrors formats and delivers an error to the Sentry server.
|
||||
// Adds a stacktrace to the packet, excluding the call to this method.
|
||||
func (client *Client) CaptureError(err error, tags map[string]string, interfaces ...Interface) string {
|
||||
if client == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if client.shouldExcludeErr(err.Error()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
extra := extractExtra(err)
|
||||
cause := pkgErrors.Cause(err)
|
||||
|
||||
packet := NewPacketWithExtra(err.Error(), extra, append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...)
|
||||
eventID, _ := client.Capture(packet, tags)
|
||||
|
||||
return eventID
|
||||
}
|
||||
|
||||
// CaptureErrors formats and delivers an error to the Sentry server using the default *Client.
|
||||
// Adds a stacktrace to the packet, excluding the call to this method.
|
||||
func CaptureError(err error, tags map[string]string, interfaces ...Interface) string {
|
||||
return DefaultClient.CaptureError(err, tags, interfaces...)
|
||||
}
|
||||
|
||||
// CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent
|
||||
func (client *Client) CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string {
|
||||
if client == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if client.shouldExcludeErr(err.Error()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
extra := extractExtra(err)
|
||||
cause := pkgErrors.Cause(err)
|
||||
|
||||
packet := NewPacketWithExtra(err.Error(), extra, append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...)
|
||||
eventID, ch := client.Capture(packet, tags)
|
||||
if eventID != "" {
|
||||
<-ch
|
||||
}
|
||||
|
||||
return eventID
|
||||
}
|
||||
|
||||
// CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent
|
||||
func CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string {
|
||||
return DefaultClient.CaptureErrorAndWait(err, tags, interfaces...)
|
||||
}
|
||||
|
||||
// CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs.
|
||||
// If an error is captured, both the error and the reported Sentry error ID are returned.
|
||||
func (client *Client) CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (err interface{}, errorID string) {
|
||||
// Note: This doesn't need to check for client, because we still want to go through the defer/recover path
|
||||
// Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the
|
||||
// *Packet just to be thrown away, this should not be the normal case. Could be refactored to
|
||||
// be completely noop though if we cared.
|
||||
defer func() {
|
||||
var packet *Packet
|
||||
err = recover()
|
||||
switch rval := err.(type) {
|
||||
case nil:
|
||||
return
|
||||
case error:
|
||||
if client.shouldExcludeErr(rval.Error()) {
|
||||
return
|
||||
}
|
||||
packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...)
|
||||
default:
|
||||
rvalStr := fmt.Sprint(rval)
|
||||
if client.shouldExcludeErr(rvalStr) {
|
||||
return
|
||||
}
|
||||
packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...)
|
||||
}
|
||||
|
||||
errorID, _ = client.Capture(packet, tags)
|
||||
}()
|
||||
|
||||
f()
|
||||
return
|
||||
}
|
||||
|
||||
// CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs.
|
||||
// If an error is captured, both the error and the reported Sentry error ID are returned.
|
||||
func CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (interface{}, string) {
|
||||
return DefaultClient.CapturePanic(f, tags, interfaces...)
|
||||
}
|
||||
|
||||
// CapturePanicAndWait is identical to CaptureError, except it blocks and assures that the event was sent
|
||||
func (client *Client) CapturePanicAndWait(f func(), tags map[string]string, interfaces ...Interface) (err interface{}, errorID string) {
|
||||
// Note: This doesn't need to check for client, because we still want to go through the defer/recover path
|
||||
// Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the
|
||||
// *Packet just to be thrown away, this should not be the normal case. Could be refactored to
|
||||
// be completely noop though if we cared.
|
||||
defer func() {
|
||||
var packet *Packet
|
||||
err = recover()
|
||||
switch rval := err.(type) {
|
||||
case nil:
|
||||
return
|
||||
case error:
|
||||
if client.shouldExcludeErr(rval.Error()) {
|
||||
return
|
||||
}
|
||||
packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...)
|
||||
default:
|
||||
rvalStr := fmt.Sprint(rval)
|
||||
if client.shouldExcludeErr(rvalStr) {
|
||||
return
|
||||
}
|
||||
packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...)
|
||||
}
|
||||
|
||||
var ch chan error
|
||||
errorID, ch = client.Capture(packet, tags)
|
||||
if errorID != "" {
|
||||
<-ch
|
||||
}
|
||||
}()
|
||||
|
||||
f()
|
||||
return
|
||||
}
|
||||
|
||||
// CapturePanicAndWait is identical to CaptureError, except it blocks and assures that the event was sent
|
||||
func CapturePanicAndWait(f func(), tags map[string]string, interfaces ...Interface) (interface{}, string) {
|
||||
return DefaultClient.CapturePanicAndWait(f, tags, interfaces...)
|
||||
}
|
||||
|
||||
func (client *Client) Close() {
|
||||
close(client.queue)
|
||||
}
|
||||
|
||||
func Close() { DefaultClient.Close() }
|
||||
|
||||
// Wait blocks and waits for all events to finish being sent to Sentry server
|
||||
func (client *Client) Wait() {
|
||||
client.wg.Wait()
|
||||
}
|
||||
|
||||
// Wait blocks and waits for all events to finish being sent to Sentry server
|
||||
func Wait() { DefaultClient.Wait() }
|
||||
|
||||
func (client *Client) URL() string {
|
||||
client.mu.RLock()
|
||||
defer client.mu.RUnlock()
|
||||
|
||||
return client.url
|
||||
}
|
||||
|
||||
func URL() string { return DefaultClient.URL() }
|
||||
|
||||
func (client *Client) ProjectID() string {
|
||||
client.mu.RLock()
|
||||
defer client.mu.RUnlock()
|
||||
|
||||
return client.projectID
|
||||
}
|
||||
|
||||
func ProjectID() string { return DefaultClient.ProjectID() }
|
||||
|
||||
func (client *Client) Release() string {
|
||||
client.mu.RLock()
|
||||
defer client.mu.RUnlock()
|
||||
|
||||
return client.release
|
||||
}
|
||||
|
||||
func Release() string { return DefaultClient.Release() }
|
||||
|
||||
func IncludePaths() []string { return DefaultClient.IncludePaths() }
|
||||
|
||||
func (client *Client) IncludePaths() []string {
|
||||
client.mu.RLock()
|
||||
defer client.mu.RUnlock()
|
||||
|
||||
return client.includePaths
|
||||
}
|
||||
|
||||
func SetIncludePaths(p []string) { DefaultClient.SetIncludePaths(p) }
|
||||
|
||||
func (client *Client) SetIncludePaths(p []string) {
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
|
||||
client.includePaths = p
|
||||
}
|
||||
|
||||
func (c *Client) SetUserContext(u *User) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.context.setUser(u)
|
||||
}
|
||||
|
||||
func (c *Client) SetHttpContext(h *Http) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.context.setHttp(h)
|
||||
}
|
||||
|
||||
func (c *Client) SetTagsContext(t map[string]string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.context.setTags(t)
|
||||
}
|
||||
|
||||
func (c *Client) ClearContext() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.context.clear()
|
||||
}
|
||||
|
||||
func SetUserContext(u *User) { DefaultClient.SetUserContext(u) }
|
||||
func SetHttpContext(h *Http) { DefaultClient.SetHttpContext(h) }
|
||||
func SetTagsContext(t map[string]string) { DefaultClient.SetTagsContext(t) }
|
||||
func ClearContext() { DefaultClient.ClearContext() }
|
||||
|
||||
// HTTPTransport is the default transport, delivering packets to Sentry via the
|
||||
// HTTP API.
|
||||
type HTTPTransport struct {
|
||||
*http.Client
|
||||
}
|
||||
|
||||
func (t *HTTPTransport) Send(url, authHeader string, packet *Packet) error {
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
body, contentType, err := serializedPacket(packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing packet: %v", err)
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create new request: %v", err)
|
||||
}
|
||||
req.Header.Set("X-Sentry-Auth", authHeader)
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
res, err := t.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("raven: got http status %d - x-sentry-error: %s", res.StatusCode, res.Header.Get("X-Sentry-Error"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func serializedPacket(packet *Packet) (io.Reader, string, error) {
|
||||
packetJSON, err := packet.JSON()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error marshaling packet %+v to JSON: %v", packet, err)
|
||||
}
|
||||
|
||||
// Only deflate/base64 the packet if it is bigger than 1KB, as there is
|
||||
// overhead.
|
||||
if len(packetJSON) > 1000 {
|
||||
buf := &bytes.Buffer{}
|
||||
b64 := base64.NewEncoder(base64.StdEncoding, buf)
|
||||
deflate, _ := zlib.NewWriterLevel(b64, zlib.BestCompression)
|
||||
deflate.Write(packetJSON)
|
||||
deflate.Close()
|
||||
b64.Close()
|
||||
return buf, "application/octet-stream", nil
|
||||
}
|
||||
return bytes.NewReader(packetJSON), "application/json", nil
|
||||
}
|
||||
|
||||
var hostname string
|
||||
|
||||
func init() {
|
||||
hostname, _ = os.Hostname()
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
package raven
|
||||
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
type errWrappedWithExtra struct {
|
||||
err error
|
||||
extraInfo map[string]interface{}
|
||||
}
|
||||
|
||||
func (ewx *errWrappedWithExtra) Error() string {
|
||||
return ewx.err.Error()
|
||||
}
|
||||
|
||||
func (ewx *errWrappedWithExtra) Cause() error {
|
||||
return ewx.err
|
||||
}
|
||||
|
||||
func (ewx *errWrappedWithExtra) ExtraInfo() Extra {
|
||||
return ewx.extraInfo
|
||||
}
|
||||
|
||||
// Adds extra data to an error before reporting to Sentry
|
||||
func WrapWithExtra(err error, extraInfo map[string]interface{}) error {
|
||||
return &errWrappedWithExtra{
|
||||
err: err,
|
||||
extraInfo: extraInfo,
|
||||
}
|
||||
}
|
||||
|
||||
type ErrWithExtra interface {
|
||||
Error() string
|
||||
Cause() error
|
||||
ExtraInfo() Extra
|
||||
}
|
||||
|
||||
// Iteratively fetches all the Extra data added to an error,
|
||||
// and it's underlying errors. Extra data defined first is
|
||||
// respected, and is not overridden when extracting.
|
||||
func extractExtra(err error) Extra {
|
||||
extra := Extra{}
|
||||
|
||||
currentErr := err
|
||||
for currentErr != nil {
|
||||
if errWithExtra, ok := currentErr.(ErrWithExtra); ok {
|
||||
for k, v := range errWithExtra.ExtraInfo() {
|
||||
extra[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if errWithCause, ok := currentErr.(causer); ok {
|
||||
currentErr = errWithCause.Cause()
|
||||
} else {
|
||||
currentErr = nil
|
||||
}
|
||||
}
|
||||
|
||||
return extra
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
package raven
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var errorMsgPattern = regexp.MustCompile(`\A(\w+): (.+)\z`)
|
||||
|
||||
func NewException(err error, stacktrace *Stacktrace) *Exception {
|
||||
msg := err.Error()
|
||||
ex := &Exception{
|
||||
Stacktrace: stacktrace,
|
||||
Value: msg,
|
||||
Type: reflect.TypeOf(err).String(),
|
||||
}
|
||||
if m := errorMsgPattern.FindStringSubmatch(msg); m != nil {
|
||||
ex.Module, ex.Value = m[1], m[2]
|
||||
}
|
||||
return ex
|
||||
}
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces
|
||||
type Exception struct {
|
||||
// Required
|
||||
Value string `json:"value"`
|
||||
|
||||
// Optional
|
||||
Type string `json:"type,omitempty"`
|
||||
Module string `json:"module,omitempty"`
|
||||
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
|
||||
}
|
||||
|
||||
func (e *Exception) Class() string { return "exception" }
|
||||
|
||||
func (e *Exception) Culprit() string {
|
||||
if e.Stacktrace == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Stacktrace.Culprit()
|
||||
}
|
||||
|
||||
// Exceptions allows for chained errors
|
||||
// https://docs.sentry.io/clientdev/interfaces/exception/
|
||||
type Exceptions struct {
|
||||
// Required
|
||||
Values []*Exception `json:"values"`
|
||||
}
|
||||
|
||||
func (es Exceptions) Class() string { return "exception" }
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
package raven
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NewHttp(req *http.Request) *Http {
|
||||
proto := "http"
|
||||
if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
proto = "https"
|
||||
}
|
||||
h := &Http{
|
||||
Method: req.Method,
|
||||
Cookies: req.Header.Get("Cookie"),
|
||||
Query: sanitizeQuery(req.URL.Query()).Encode(),
|
||||
URL: proto + "://" + req.Host + req.URL.Path,
|
||||
Headers: make(map[string]string, len(req.Header)),
|
||||
}
|
||||
if addr, port, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
h.Env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
|
||||
}
|
||||
for k, v := range req.Header {
|
||||
h.Headers[k] = strings.Join(v, ",")
|
||||
}
|
||||
h.Headers["Host"] = req.Host
|
||||
return h
|
||||
}
|
||||
|
||||
var querySecretFields = []string{"password", "passphrase", "passwd", "secret"}
|
||||
|
||||
func sanitizeQuery(query url.Values) url.Values {
|
||||
for _, keyword := range querySecretFields {
|
||||
for field := range query {
|
||||
if strings.Contains(field, keyword) {
|
||||
query[field] = []string{"********"}
|
||||
}
|
||||
}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces
|
||||
type Http struct {
|
||||
// Required
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Query string `json:"query_string,omitempty"`
|
||||
|
||||
// Optional
|
||||
Cookies string `json:"cookies,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
|
||||
// Must be either a string or map[string]string
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Http) Class() string { return "request" }
|
||||
|
||||
// Recovery handler to wrap the stdlib net/http Mux.
|
||||
// Example:
|
||||
// http.HandleFunc("/", raven.RecoveryHandler(func(w http.ResponseWriter, r *http.Request) {
|
||||
// ...
|
||||
// }))
|
||||
func RecoveryHandler(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return Recoverer(http.HandlerFunc(handler)).ServeHTTP
|
||||
}
|
||||
|
||||
// Recovery handler to wrap the stdlib net/http Mux.
|
||||
// Example:
|
||||
// mux := http.NewServeMux
|
||||
// ...
|
||||
// http.Handle("/", raven.Recoverer(mux))
|
||||
func Recoverer(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if rval := recover(); rval != nil {
|
||||
debug.PrintStack()
|
||||
rvalStr := fmt.Sprint(rval)
|
||||
var packet *Packet
|
||||
if err, ok := rval.(error); ok {
|
||||
packet = NewPacket(rvalStr, NewException(errors.New(rvalStr), GetOrNewStacktrace(err, 2, 3, nil)), NewHttp(r))
|
||||
} else {
|
||||
packet = NewPacket(rvalStr, NewException(errors.New(rvalStr), NewStacktrace(2, 3, nil)), NewHttp(r))
|
||||
}
|
||||
Capture(packet, nil)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
package raven
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#message-interface
|
||||
type Message struct {
|
||||
// Required
|
||||
Message string `json:"message"`
|
||||
|
||||
// Optional
|
||||
Params []interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Message) Class() string { return "logentry" }
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#template-interface
|
||||
type Template struct {
|
||||
// Required
|
||||
Filename string `json:"filename"`
|
||||
Lineno int `json:"lineno"`
|
||||
ContextLine string `json:"context_line"`
|
||||
|
||||
// Optional
|
||||
PreContext []string `json:"pre_context,omitempty"`
|
||||
PostContext []string `json:"post_context,omitempty"`
|
||||
AbsolutePath string `json:"abs_path,omitempty"`
|
||||
}
|
||||
|
||||
func (t *Template) Class() string { return "template" }
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces
|
||||
type User struct {
|
||||
// All fields are optional
|
||||
ID string `json:"id,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
IP string `json:"ip_address,omitempty"`
|
||||
}
|
||||
|
||||
func (h *User) Class() string { return "user" }
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces
|
||||
type Query struct {
|
||||
// Required
|
||||
Query string `json:"query"`
|
||||
|
||||
// Optional
|
||||
Engine string `json:"engine,omitempty"`
|
||||
}
|
||||
|
||||
func (q *Query) Class() string { return "query" }
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
go test -race ./...
|
||||
go test -cover ./...
|
||||
go test -v ./...
|
||||
-277
@@ -1,277 +0,0 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
// Some code from the runtime/debug package of the Go standard library.
|
||||
|
||||
package raven
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/build"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces
|
||||
type Stacktrace struct {
|
||||
// Required
|
||||
Frames []*StacktraceFrame `json:"frames"`
|
||||
}
|
||||
|
||||
func (s *Stacktrace) Class() string { return "stacktrace" }
|
||||
|
||||
func (s *Stacktrace) Culprit() string {
|
||||
for i := len(s.Frames) - 1; i >= 0; i-- {
|
||||
frame := s.Frames[i]
|
||||
if frame.InApp == true && frame.Module != "" && frame.Function != "" {
|
||||
return frame.Module + "." + frame.Function
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type StacktraceFrame struct {
|
||||
// At least one required
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Function string `json:"function,omitempty"`
|
||||
Module string `json:"module,omitempty"`
|
||||
|
||||
// Optional
|
||||
Lineno int `json:"lineno,omitempty"`
|
||||
Colno int `json:"colno,omitempty"`
|
||||
AbsolutePath string `json:"abs_path,omitempty"`
|
||||
ContextLine string `json:"context_line,omitempty"`
|
||||
PreContext []string `json:"pre_context,omitempty"`
|
||||
PostContext []string `json:"post_context,omitempty"`
|
||||
InApp bool `json:"in_app"`
|
||||
}
|
||||
|
||||
// Try to get stacktrace from err as an interface of github.com/pkg/errors, or else NewStacktrace()
|
||||
func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []string) *Stacktrace {
|
||||
stacktracer, errHasStacktrace := err.(interface {
|
||||
StackTrace() errors.StackTrace
|
||||
})
|
||||
if errHasStacktrace {
|
||||
var frames []*StacktraceFrame
|
||||
for _, f := range stacktracer.StackTrace() {
|
||||
pc := uintptr(f) - 1
|
||||
fn := runtime.FuncForPC(pc)
|
||||
var fName string
|
||||
var file string
|
||||
var line int
|
||||
if fn != nil {
|
||||
file, line = fn.FileLine(pc)
|
||||
fName = fn.Name()
|
||||
} else {
|
||||
file = "unknown"
|
||||
fName = "unknown"
|
||||
}
|
||||
frame := NewStacktraceFrame(pc, fName, file, line, context, appPackagePrefixes)
|
||||
if frame != nil {
|
||||
frames = append([]*StacktraceFrame{frame}, frames...)
|
||||
}
|
||||
}
|
||||
return &Stacktrace{Frames: frames}
|
||||
} else {
|
||||
return NewStacktrace(skip+1, context, appPackagePrefixes)
|
||||
}
|
||||
}
|
||||
|
||||
// Intialize and populate a new stacktrace, skipping skip frames.
|
||||
//
|
||||
// context is the number of surrounding lines that should be included for context.
|
||||
// Setting context to 3 would try to get seven lines. Setting context to -1 returns
|
||||
// one line with no surrounding context, and 0 returns no context.
|
||||
//
|
||||
// appPackagePrefixes is a list of prefixes used to check whether a package should
|
||||
// be considered "in app".
|
||||
func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace {
|
||||
var frames []*StacktraceFrame
|
||||
|
||||
callerPcs := make([]uintptr, 100)
|
||||
numCallers := runtime.Callers(skip+2, callerPcs)
|
||||
|
||||
// If there are no callers, the entire stacktrace is nil
|
||||
if numCallers == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
callersFrames := runtime.CallersFrames(callerPcs)
|
||||
|
||||
for {
|
||||
fr, more := callersFrames.Next()
|
||||
if fr.Func != nil {
|
||||
frame := NewStacktraceFrame(fr.PC, fr.Function, fr.File, fr.Line, context, appPackagePrefixes)
|
||||
if frame != nil {
|
||||
frames = append(frames, frame)
|
||||
}
|
||||
}
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
// If there are no frames, the entire stacktrace is nil
|
||||
if len(frames) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Optimize the path where there's only 1 frame
|
||||
if len(frames) == 1 {
|
||||
return &Stacktrace{frames}
|
||||
}
|
||||
// Sentry wants the frames with the oldest first, so reverse them
|
||||
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
|
||||
frames[i], frames[j] = frames[j], frames[i]
|
||||
}
|
||||
return &Stacktrace{frames}
|
||||
}
|
||||
|
||||
// Build a single frame using data returned from runtime.Caller.
|
||||
//
|
||||
// context is the number of surrounding lines that should be included for context.
|
||||
// Setting context to 3 would try to get seven lines. Setting context to -1 returns
|
||||
// one line with no surrounding context, and 0 returns no context.
|
||||
//
|
||||
// appPackagePrefixes is a list of prefixes used to check whether a package should
|
||||
// be considered "in app".
|
||||
func NewStacktraceFrame(pc uintptr, fName, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame {
|
||||
frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line, InApp: false}
|
||||
frame.Module, frame.Function = functionName(fName)
|
||||
|
||||
// `runtime.goexit` is effectively a placeholder that comes from
|
||||
// runtime/asm_amd64.s and is meaningless.
|
||||
if frame.Module == "runtime" && frame.Function == "goexit" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if frame.Module == "main" {
|
||||
frame.InApp = true
|
||||
} else {
|
||||
for _, prefix := range appPackagePrefixes {
|
||||
if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") {
|
||||
frame.InApp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if context > 0 {
|
||||
contextLines, lineIdx := sourceCodeLoader.Load(file, line, context)
|
||||
if len(contextLines) > 0 {
|
||||
for i, line := range contextLines {
|
||||
switch {
|
||||
case i < lineIdx:
|
||||
frame.PreContext = append(frame.PreContext, string(line))
|
||||
case i == lineIdx:
|
||||
frame.ContextLine = string(line)
|
||||
default:
|
||||
frame.PostContext = append(frame.PostContext, string(line))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if context == -1 {
|
||||
contextLine, _ := sourceCodeLoader.Load(file, line, 0)
|
||||
if len(contextLine) > 0 {
|
||||
frame.ContextLine = string(contextLine[0])
|
||||
}
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
// Retrieve the name of the package and function containing the PC.
|
||||
func functionName(fName string) (pack string, name string) {
|
||||
name = fName
|
||||
// We get this:
|
||||
// runtime/debug.*T·ptrmethod
|
||||
// and want this:
|
||||
// pack = runtime/debug
|
||||
// name = *T.ptrmethod
|
||||
if idx := strings.LastIndex(name, "."); idx != -1 {
|
||||
pack = name[:idx]
|
||||
name = name[idx+1:]
|
||||
}
|
||||
name = strings.Replace(name, "·", ".", -1)
|
||||
return
|
||||
}
|
||||
|
||||
type SourceCodeLoader interface {
|
||||
Load(filename string, line, context int) ([][]byte, int)
|
||||
}
|
||||
|
||||
var sourceCodeLoader SourceCodeLoader = &fsLoader{cache: make(map[string][][]byte)}
|
||||
|
||||
func SetSourceCodeLoader(loader SourceCodeLoader) {
|
||||
sourceCodeLoader = loader
|
||||
}
|
||||
|
||||
type fsLoader struct {
|
||||
mu sync.Mutex
|
||||
cache map[string][][]byte
|
||||
}
|
||||
|
||||
func (fs *fsLoader) Load(filename string, line, context int) ([][]byte, int) {
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
lines, ok := fs.cache[filename]
|
||||
if !ok {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
// cache errors as nil slice: code below handles it correctly
|
||||
// otherwise when missing the source or running as a different user, we try
|
||||
// reading the file on each error which is unnecessary
|
||||
fs.cache[filename] = nil
|
||||
return nil, 0
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
fs.cache[filename] = lines
|
||||
}
|
||||
|
||||
if lines == nil {
|
||||
// cached error from ReadFile: return no lines
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
line-- // stack trace lines are 1-indexed
|
||||
start := line - context
|
||||
var idx int
|
||||
if start < 0 {
|
||||
start = 0
|
||||
idx = line
|
||||
} else {
|
||||
idx = context
|
||||
}
|
||||
end := line + context + 1
|
||||
if line >= len(lines) {
|
||||
return nil, 0
|
||||
}
|
||||
if end > len(lines) {
|
||||
end = len(lines)
|
||||
}
|
||||
return lines[start:end], idx
|
||||
}
|
||||
|
||||
var trimPaths []string
|
||||
|
||||
// Try to trim the GOROOT or GOPATH prefix off of a filename
|
||||
func trimPath(filename string) string {
|
||||
for _, prefix := range trimPaths {
|
||||
if trimmed := strings.TrimPrefix(filename, prefix); len(trimmed) < len(filename) {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Collect all source directories, and make sure they
|
||||
// end in a trailing "separator"
|
||||
for _, prefix := range build.Default.SrcDirs() {
|
||||
if prefix[len(prefix)-1] != filepath.Separator {
|
||||
prefix += string(filepath.Separator)
|
||||
}
|
||||
trimPaths = append(trimPaths, prefix)
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package raven
|
||||
|
||||
type Writer struct {
|
||||
Client *Client
|
||||
Level Severity
|
||||
Logger string // Logger name reported to Sentry
|
||||
}
|
||||
|
||||
// Write formats the byte slice p into a string, and sends a message to
|
||||
// Sentry at the severity level indicated by the Writer w.
|
||||
func (w *Writer) Write(p []byte) (int, error) {
|
||||
message := string(p)
|
||||
|
||||
packet := NewPacket(message, &Message{message, nil})
|
||||
packet.Level = w.Level
|
||||
packet.Logger = w.Logger
|
||||
w.Client.Capture(packet, nil)
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
Vendored
-6
@@ -8,9 +8,6 @@ github.com/apparentlymart/go-cidr/cidr
|
||||
# github.com/beorn7/perks v1.0.1
|
||||
## explicit; go 1.11
|
||||
github.com/beorn7/perks/quantile
|
||||
# github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d
|
||||
## explicit; go 1.12
|
||||
github.com/certifi/gocertifi
|
||||
# github.com/cespare/xxhash/v2 v2.1.2
|
||||
## explicit; go 1.11
|
||||
github.com/cespare/xxhash/v2
|
||||
@@ -109,9 +106,6 @@ github.com/flynn/go-shlex
|
||||
# github.com/fsnotify/fsnotify v1.4.9
|
||||
## explicit; go 1.13
|
||||
github.com/fsnotify/fsnotify
|
||||
# github.com/getsentry/raven-go v0.2.0
|
||||
## explicit
|
||||
github.com/getsentry/raven-go
|
||||
# github.com/getsentry/sentry-go v0.16.0
|
||||
## explicit; go 1.19
|
||||
github.com/getsentry/sentry-go
|
||||
|
||||
Reference in New Issue
Block a user