mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 23:31:56 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71d66ae7ee | |||
| 133e6fdc88 | |||
| 1d5cc45ac7 | |||
| a412f629c2 | |||
| 979e5be8ab | |||
| 2789d0cf36 | |||
| 5bcb2da0fe | |||
| 4f23da2a6d | |||
| fe032843f3 | |||
| ff795a7beb | |||
| 40d9370bb6 | |||
| 02f0ed951f | |||
| c2a71c5a51 | |||
| 945bf76897 | |||
| d3b254f9ae | |||
| ee588eeeaa | |||
| dd521aba29 | |||
| a06390a078 | |||
| 2cac1f0f78 | |||
| a10785a701 | |||
| dc730615f2 | |||
| b836cb350a | |||
| 5e85a8bd16 | |||
| f41121f4e5 | |||
| 93a9504824 | |||
| 7abbe91d41 | |||
| cf314ddb58 | |||
| ff97fb6dc8 | |||
| dc48cdce1a | |||
| f130e6d4d7 | |||
| ef5b44b2d0 | |||
| 858ef29868 | |||
| baec3e289e | |||
| df25ed9bde | |||
| 4e1df1a211 | |||
| c3c88cc31e | |||
| fb8ff33203 | |||
| 188f4667cb | |||
| 30c9e2af9b |
+5
-1
@@ -9,4 +9,8 @@ guide/public
|
||||
\#*\#
|
||||
cscope.*
|
||||
cloudflared
|
||||
!cmd/cloudflared/
|
||||
cloudflared.exe
|
||||
!cmd/cloudflared/
|
||||
.DS_Store
|
||||
*-session.log
|
||||
ssh_server_tests/.env
|
||||
|
||||
Generated
+94
-173
@@ -1,14 +1,6 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3d3a509c5ba327e8573bb57f9da8430c63a46a06886eb1d2ffc8af4e76f31c72"
|
||||
name = "cloud.google.com/go"
|
||||
packages = ["civil"]
|
||||
pruneopts = "UT"
|
||||
revision = "cdaaf98f9226c39dc162b8e55083b2fbc67b4674"
|
||||
version = "v0.43.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9f3b30d9f8e0d7040f729b82dcbc8f0dead820a133b3147ce355fc451f32d761"
|
||||
name = "github.com/BurntSushi/toml"
|
||||
@@ -18,12 +10,57 @@
|
||||
version = "v0.3.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c84a587136cb69cecc11f3dbe9f9001444044c0dba74997b07f7e4c150b07cda"
|
||||
name = "github.com/DATA-DOG/go-sqlmock"
|
||||
branch = "master"
|
||||
digest = "1:1a200e7e73293b75eb8e5c93d023b5472663432da0b663e1532624fcfede9ca8"
|
||||
name = "github.com/anmitsu/go-shlex"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "3f9954f6f6697845b082ca57995849ddf614f450"
|
||||
version = "v1.3.3"
|
||||
revision = "648efa622239a2f6ff949fed78ee37b48d499ba4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4a296b8f487dab7ea37c4558d6e84be8103eb6bd297202e870266ff99ebd769d"
|
||||
name = "github.com/aws/aws-sdk-go"
|
||||
packages = [
|
||||
"aws",
|
||||
"aws/awserr",
|
||||
"aws/awsutil",
|
||||
"aws/client",
|
||||
"aws/client/metadata",
|
||||
"aws/corehandlers",
|
||||
"aws/credentials",
|
||||
"aws/credentials/ec2rolecreds",
|
||||
"aws/credentials/endpointcreds",
|
||||
"aws/credentials/processcreds",
|
||||
"aws/credentials/stscreds",
|
||||
"aws/csm",
|
||||
"aws/defaults",
|
||||
"aws/ec2metadata",
|
||||
"aws/endpoints",
|
||||
"aws/request",
|
||||
"aws/session",
|
||||
"aws/signer/v4",
|
||||
"internal/ini",
|
||||
"internal/s3err",
|
||||
"internal/sdkio",
|
||||
"internal/sdkrand",
|
||||
"internal/sdkuri",
|
||||
"internal/shareddefaults",
|
||||
"private/protocol",
|
||||
"private/protocol/eventstream",
|
||||
"private/protocol/eventstream/eventstreamapi",
|
||||
"private/protocol/json/jsonutil",
|
||||
"private/protocol/query",
|
||||
"private/protocol/query/queryutil",
|
||||
"private/protocol/rest",
|
||||
"private/protocol/restxml",
|
||||
"private/protocol/xml/xmlutil",
|
||||
"service/s3",
|
||||
"service/sts",
|
||||
"service/sts/stsiface",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "2eb9a651f22769d792f974e8a8efe16afea3370f"
|
||||
version = "v1.23.9"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
|
||||
@@ -55,14 +92,6 @@
|
||||
pruneopts = "UT"
|
||||
revision = "333127dbecfcc23a8db7d9a4f52785d23aff44a1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:05756b73bf7eae03d6f6c5f0ae83de42ce296e7d83336563dd9ce2072106d5e6"
|
||||
name = "github.com/cloudflare/golz4"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ef862a3cdc58a6f1fee4e3af3d44fbe279194cde"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3f9506ee991cdee1f05bf0cd3e34b5cd922dc00d6a950fb4beb4e07ab1c4d3d1"
|
||||
name = "github.com/coredns/coredns"
|
||||
@@ -97,12 +126,11 @@
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
digest = "1:ca6ce07b0d28c6044411b9c966cd845233e27c2ff91d08a3a869631f329c9918"
|
||||
digest = "1:6f70106e7bc1c803e8a0a4519e09c12d154771acfa2559206e97b033bbd1dd38"
|
||||
name = "github.com/coreos/go-oidc"
|
||||
packages = ["."]
|
||||
packages = ["jose"]
|
||||
pruneopts = "UT"
|
||||
revision = "274971e2c94cd6fb17614979e2899edbc81af146"
|
||||
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1da3a221f0bc090792d3a2a080ff09008427c0e0f0533a4ed6abd8994421da73"
|
||||
@@ -112,6 +140,14 @@
|
||||
revision = "95778dfbb74eb7e4dbaf43bf7d71809650ef8076"
|
||||
version = "v19"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:385dff5bf83bf1c9d40ef2b0f2e697f0d1973fc9e24ae9401cf41698612bbebc"
|
||||
name = "github.com/creack/pty"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "2769f65a3a94eb8f876f44a0459d24ae7ad2e488"
|
||||
version = "v1.1.7"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
@@ -122,15 +158,11 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:27cbe99893a40975358d20736d80adc95ef395d3bf5923f802f3de259533c94a"
|
||||
name = "github.com/denisenkom/go-mssqldb"
|
||||
packages = [
|
||||
".",
|
||||
"internal/cp",
|
||||
"internal/querytext",
|
||||
]
|
||||
digest = "1:c013ffc6e15f9f898078f9d38441c68b228aa7b899659452170250ccb27f5f1e"
|
||||
name = "github.com/elgs/gosqljson"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "11b2859924c1e3c3da056fd4e0976b93883f3545"
|
||||
revision = "027aa4915315a0b2825c0f025cea347829b974fa"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d4268b2a09b1f736633577c4ac93f2a5356c73742fff5344e2451aeec60a7ad0"
|
||||
@@ -154,14 +186,6 @@
|
||||
pruneopts = "UT"
|
||||
revision = "75cf19382434e82df4dd84953f566b8ad23d6e9e"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:296cda2c4a6a7a54964d7b3b0815e2aed215da75217b72033060ffb1c4e4b6ae"
|
||||
name = "github.com/felixge/httpsnoop"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "eadd4fad6aac69ae62379194fe0219f3dbc80fd3"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:50a46ab1d5edbbdd55125b4d37f1bf503d0807c26461f9ad7b358d6006641d09"
|
||||
@@ -178,12 +202,12 @@
|
||||
revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ec6f9bf5e274c833c911923c9193867f3f18788c461f76f05f62bb1510e0ae65"
|
||||
name = "github.com/go-sql-driver/mysql"
|
||||
digest = "1:0a01355005024757ae2a1f62e8fe68a30b4f5fde7a08ac88b3685e7999ac354b"
|
||||
name = "github.com/gliderlabs/ssh"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "72cd26f257d44c1114970e19afddcd812016007e"
|
||||
version = "v1.4.1"
|
||||
revision = "5b6cc7030f17095c0cf23bb063b0bfe824fe5f8b"
|
||||
version = "v0.2.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -193,14 +217,6 @@
|
||||
pruneopts = "UT"
|
||||
revision = "604e922904d35e97f98a774db7881f049cd8d970"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:01745416ed734d0dbd8f2a25344536e399ee60319654873da12b107e9e3cb309"
|
||||
name = "github.com/golang/gddo"
|
||||
packages = ["httputil/header"]
|
||||
pruneopts = "UT"
|
||||
revision = "af0f2af80721261f4d211b2c9563f7b46b2aab06"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:239c4c7fd2159585454003d9be7207167970194216193a8a210b8d29576f19c9"
|
||||
name = "github.com/golang/protobuf"
|
||||
@@ -247,6 +263,13 @@
|
||||
pruneopts = "UT"
|
||||
revision = "8e809c8a86450a29b90dcc9efbf062d0fe6d9746"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bb81097a5b62634f3e9fec1014657855610c82d19b9a40c17612e32651e35dca"
|
||||
name = "github.com/jmespath/go-jmespath"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "c2b33e84"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6c41d4f998a03b6604227ccad36edaed6126c397e5d78709ef4814a1145a6757"
|
||||
name = "github.com/jmoiron/sqlx"
|
||||
@@ -267,26 +290,7 @@
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:177a18252d40bf39a867876456854717780b0b1212fdae231d31e93b6f885d80"
|
||||
name = "github.com/kshvakov/clickhouse"
|
||||
packages = [
|
||||
".",
|
||||
"lib/binary",
|
||||
"lib/cityhash102",
|
||||
"lib/column",
|
||||
"lib/data",
|
||||
"lib/leakypool",
|
||||
"lib/lz4",
|
||||
"lib/protocol",
|
||||
"lib/types",
|
||||
"lib/writebuffer",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "43e176a8d165376662f4d67203c71cc8a027c86b"
|
||||
version = "v1.3.9"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:12cb143f2148bf54bcd9fe622abac17325e85eeb1d84b8ec6caf1c80232108fd"
|
||||
digest = "1:bc1c0be40c67b6b4aee09d7508d5a2a52c1c116b1fa43806dad2b0d6b4d4003b"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
@@ -294,8 +298,8 @@
|
||||
"scram",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "3427c32cb71afc948325f299f040e53c1dd78979"
|
||||
version = "v1.2.0"
|
||||
revision = "51e2106eed1cea199c802d2a49e91e2491b02056"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2fa7b0155cd54479a755c629de26f888a918e13f8857a2c442205d825368e084"
|
||||
@@ -313,14 +317,6 @@
|
||||
revision = "c2a7a6ca930a4cd0bc33a3f298eb71960732a3a7"
|
||||
version = "v0.0.7"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:79e87abf06b873987dee86598950f5b51732ac454d5a5cab6445a14330e6c9e3"
|
||||
name = "github.com/mattn/go-sqlite3"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "b612a2feea6aa87c6d052d9086572551df06497e"
|
||||
version = "v1.11.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc"
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
@@ -356,14 +352,6 @@
|
||||
revision = "af06845cf3004701891bf4fdb884bfe4920b3727"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:dd3d50b46f8d3c88a8d3986f2e5344db582c457469a7cb3a0dda953c669cdf9a"
|
||||
name = "github.com/mitchellh/go-server-timing"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "1ca01db910d57d64ec1c68f27105614b77558e28"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
@@ -400,17 +388,6 @@
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:bd9efe4e0b0f768302a1e2f0c22458149278de533e521206e5ddc71848c269a0"
|
||||
name = "github.com/pquerna/cachecontrol"
|
||||
packages = [
|
||||
".",
|
||||
"cacheobject",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "1555304b9b35fdd2b425bccf1a5613677705e7d0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c968b29db5d68ec97de404b6d058d5937fa015a141b3b4f7a0d87d5f8226f04c"
|
||||
name = "github.com/prometheus/client_golang"
|
||||
@@ -459,9 +436,12 @@
|
||||
version = "v2.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:04457f9f6f3ffc5fea48e71d62f2ca256637dee0a04d710288e27e05c8b41976"
|
||||
digest = "1:31538f8d774e8bf2fb9380d2d2a82d69e206a7d0603946586926f1819c546c26"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
packages = [
|
||||
".",
|
||||
"hooks/test",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
|
||||
version = "v1.4.2"
|
||||
@@ -479,15 +459,7 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:cac263b8eb2e9ad2327b70e03be1de1227040bfb3be26b49c4b56cbed568afc7"
|
||||
name = "github.com/xo/dburl"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "98997a05b24fc069c79fa13b05ccf376c22a326c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:1a9391a80fe548ffcf90aee8ae35bfb0b188198b57a3fe2ef5bb099ba4b4f610"
|
||||
digest = "1:a84d5ec8b40a827962ea250f2cf03434138ccae9d83fcac12fb49b70c70b80cc"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"curve25519",
|
||||
@@ -495,10 +467,8 @@
|
||||
"ed25519/internal/edwards25519",
|
||||
"internal/chacha20",
|
||||
"internal/subtle",
|
||||
"md4",
|
||||
"nacl/box",
|
||||
"nacl/secretbox",
|
||||
"pbkdf2",
|
||||
"poly1305",
|
||||
"salsa20/salsa",
|
||||
"ssh",
|
||||
@@ -509,12 +479,11 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:6b3dbfffba73ea0d8131ecb5738fe87f6addc4c7b73154b9d58e2693ad6a6256"
|
||||
digest = "1:52d140f7ab52e491cc1cbc93e6637aa5e9a7f3beae7545d675b02e52ca9d7290"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"bpf",
|
||||
"context",
|
||||
"context/ctxhttp",
|
||||
"http/httpguts",
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
@@ -530,17 +499,6 @@
|
||||
pruneopts = "UT"
|
||||
revision = "1da14a5a36f220ea3f03470682b737b1dfd5de22"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:8d1c112fb1679fa097e9a9255a786ee47383fa2549a3da71bcb1334a693ebcfe"
|
||||
name = "golang.org/x/oauth2"
|
||||
packages = [
|
||||
".",
|
||||
"internal",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "0f29369cfe4552d0e4bcddc57cc75f4d7e672a33"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239"
|
||||
name = "golang.org/x/sync"
|
||||
@@ -587,23 +545,6 @@
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7e8b9c5ae49011b12ae8473834ac1a7bb8ac029ba201270c723e4c280c9e4855"
|
||||
name = "google.golang.org/appengine"
|
||||
packages = [
|
||||
"cloudsql",
|
||||
"internal",
|
||||
"internal/base",
|
||||
"internal/datastore",
|
||||
"internal/log",
|
||||
"internal/remote_api",
|
||||
"internal/urlfetch",
|
||||
"urlfetch",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "b2f4a3cf3c67576a2ee09e1fe62656a5086ce880"
|
||||
version = "v1.6.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:c3076e7defee87de1236f1814beb588f40a75544c60121e6eb38b3b3721783e2"
|
||||
@@ -653,26 +594,6 @@
|
||||
revision = "236199dd5f8031d698fb64091194aecd1c3895b2"
|
||||
version = "v1.20.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "v1"
|
||||
digest = "1:6f70106e7bc1c803e8a0a4519e09c12d154771acfa2559206e97b033bbd1dd38"
|
||||
name = "gopkg.in/coreos/go-oidc.v1"
|
||||
packages = ["jose"]
|
||||
pruneopts = "UT"
|
||||
revision = "e860bd55bfa7d7cb35d30d26a167982584f616b0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8c05919580be8a5be668709d7e5a69d5cd19b8ee9f23d62ce5b10d3457bf6a13"
|
||||
name = "gopkg.in/square/go-jose.v2"
|
||||
packages = [
|
||||
".",
|
||||
"cipher",
|
||||
"json",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "730df5f748271903322feb182be83b43ebbbe27d"
|
||||
version = "v2.3.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "altsrc-parse-durations"
|
||||
digest = "1:0370b1bceda03dbfade3abbde639a43f1113bab711ec760452e5c0dcc0c14787"
|
||||
@@ -722,6 +643,10 @@
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/DATA-DOG/go-sqlmock",
|
||||
"github.com/aws/aws-sdk-go/aws",
|
||||
"github.com/aws/aws-sdk-go/aws/credentials",
|
||||
"github.com/aws/aws-sdk-go/aws/session",
|
||||
"github.com/aws/aws-sdk-go/service/s3",
|
||||
"github.com/cloudflare/brotli-go",
|
||||
"github.com/cloudflare/golibs/lrucache",
|
||||
"github.com/coredns/coredns/core/dnsserver",
|
||||
@@ -731,34 +656,31 @@
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest",
|
||||
"github.com/coredns/coredns/plugin/pkg/rcode",
|
||||
"github.com/coredns/coredns/request",
|
||||
"github.com/coreos/go-oidc",
|
||||
"github.com/coreos/go-oidc/jose",
|
||||
"github.com/coreos/go-systemd/daemon",
|
||||
"github.com/denisenkom/go-mssqldb",
|
||||
"github.com/creack/pty",
|
||||
"github.com/elgs/gosqljson",
|
||||
"github.com/equinox-io/equinox",
|
||||
"github.com/facebookgo/grace/gracenet",
|
||||
"github.com/getsentry/raven-go",
|
||||
"github.com/go-sql-driver/mysql",
|
||||
"github.com/gliderlabs/ssh",
|
||||
"github.com/golang-collections/collections/queue",
|
||||
"github.com/google/uuid",
|
||||
"github.com/gorilla/mux",
|
||||
"github.com/gorilla/websocket",
|
||||
"github.com/jmoiron/sqlx",
|
||||
"github.com/kshvakov/clickhouse",
|
||||
"github.com/lib/pq",
|
||||
"github.com/mattn/go-colorable",
|
||||
"github.com/mattn/go-sqlite3",
|
||||
"github.com/miekg/dns",
|
||||
"github.com/mitchellh/go-homedir",
|
||||
"github.com/mitchellh/go-server-timing",
|
||||
"github.com/mitchellh/mapstructure",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/prometheus/client_golang/prometheus",
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
||||
"github.com/rifflock/lfshook",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/sirupsen/logrus/hooks/test",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/stretchr/testify/require",
|
||||
"github.com/xo/dburl",
|
||||
"golang.org/x/crypto/nacl/box",
|
||||
"golang.org/x/crypto/ssh",
|
||||
"golang.org/x/crypto/ssh/terminal",
|
||||
@@ -773,7 +695,6 @@
|
||||
"golang.org/x/sys/windows/svc",
|
||||
"golang.org/x/sys/windows/svc/eventlog",
|
||||
"golang.org/x/sys/windows/svc/mgr",
|
||||
"gopkg.in/coreos/go-oidc.v1/jose",
|
||||
"gopkg.in/urfave/cli.v2",
|
||||
"gopkg.in/urfave/cli.v2/altsrc",
|
||||
"zombiezen.com/go/capnproto2",
|
||||
|
||||
+12
-26
@@ -6,6 +6,8 @@
|
||||
name = "github.com/cloudflare/brotli-go"
|
||||
unused-packages = false
|
||||
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/facebookgo/grace"
|
||||
revision = "75cf19382434e82df4dd84953f566b8ad23d6e9e"
|
||||
@@ -68,6 +70,10 @@
|
||||
name = "github.com/mholt/caddy"
|
||||
revision = "d3b731e9255b72d4571a5aac125634cf1b6031dc"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coreos/go-oidc"
|
||||
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/crypto"
|
||||
branch = "master" # master required by github.com/miekg/dns
|
||||
@@ -85,33 +91,13 @@
|
||||
version = "1.1.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jmoiron/sqlx"
|
||||
version = "1.2.0"
|
||||
name = "github.com/gliderlabs/ssh"
|
||||
version = "0.2.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-sql-driver/mysql"
|
||||
version = "1.4.1"
|
||||
name = "github.com/creack/pty"
|
||||
version = "1.1.7"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mattn/go-sqlite3"
|
||||
version = "1.11.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/denisenkom/go-mssqldb"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/kshvakov/clickhouse"
|
||||
version = "1.3.9"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/DATA-DOG/go-sqlmock"
|
||||
version = "1.3.3"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/xo/dburl"
|
||||
|
||||
[[constraint]]
|
||||
branch = "v2"
|
||||
name = "github.com/coreos/go-oidc"
|
||||
name = "github.com/aws/aws-sdk-go"
|
||||
version = "1.23.9"
|
||||
|
||||
@@ -39,6 +39,10 @@ container:
|
||||
test: vet
|
||||
go test -v -race $(VERSION_FLAGS) ./...
|
||||
|
||||
.PHONY: test-ssh-server
|
||||
test-ssh-server:
|
||||
docker-compose -f ssh_server_tests/docker-compose.yml up
|
||||
|
||||
.PHONY: cloudflared-deb
|
||||
cloudflared-deb: cloudflared
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
|
||||
@@ -1,3 +1,44 @@
|
||||
2019.9.1
|
||||
- 2019-09-23 TUN-2334: remove tlsConfig.ServerName special case
|
||||
- 2019-09-23 AUTH-2077: Quotes open browser command in windows
|
||||
- 2019-09-11 AUTH-2050: Adds time.sleep to temporarily avoid hitting tunnel muxer dealock issue
|
||||
- 2019-09-10 AUTH-2056: Writes stderr to its own stream for non-pty connections
|
||||
- 2019-09-16 TUN-2307: Capnp is the only serialization format used in tunnelpogs
|
||||
- 2019-09-18 TUN-2315: Replace Scope with IntentLabel
|
||||
- 2019-09-17 TUN-2309: Split ConnectResult into ConnectError and ConnectSuccess, each implementing its own capnp serialization logic
|
||||
- 2019-09-18 AUTH-2052: Adds tests for SSH server
|
||||
- 2019-09-18 AUTH-2067: Log commands correctly
|
||||
- 2019-09-19 AUTH-2055: Verifies token at edge on access login
|
||||
- 2019-09-04 TUN-2201: change SRV records used by cloudflared
|
||||
- 2019-09-06 TUN-2280: Revert "TUN-2260: add name/group to CapnpConnectParameters, remove Scope"
|
||||
- 2019-09-03 AUTH-1943 hooked up uploader to logger, added timestamp to session logs, add tests
|
||||
- 2019-09-04 AUTH-2036: Refactor user retrieval, shutdown after ssh server stops, add custom version string
|
||||
- 2019-09-06 AUTH-1942 added event log to ssh server
|
||||
- 2019-09-04 AUTH-2037: Adds support for ssh port forwarding
|
||||
- 2019-09-05 TUN-2276: Path encoding broken
|
||||
|
||||
2019.9.0
|
||||
- 2019-09-05 TUN-2279: Revert path encoding fix
|
||||
- 2019-08-30 AUTH-2021 - check error for failing tests
|
||||
- 2019-08-29 AUTH-2030: Support both authorized_key and short lived cert authentication simultaniously without specifiying at start time
|
||||
- 2019-08-29 AUTH-2026: Adds support for non-pty sessions and inline command exec
|
||||
- 2019-08-26 AUTH-1943: Adds session logging
|
||||
- 2019-08-26 TUN-2162: Decomplect OpenStream to allow finer-grained timeouts
|
||||
- 2019-08-29 TUN-2260: add name/group to CapnpConnectParameters, remove Scope
|
||||
|
||||
2019.8.4
|
||||
- 2019-08-30 Fix #111: Add support for specifying a specific HTTP Host: header on the origin. (#114)
|
||||
- 2019-08-22 TUN-2165: Add ClientConfig to tunnelrpc.ConnectResult
|
||||
- 2019-08-20 AUTH-2014: Checks users login shell
|
||||
- 2019-08-26 TUN-2243: Revert "STOR-519: Add db-connect, a SQL over HTTPS server"
|
||||
- 2019-08-27 TUN-2244: Add NO_AUTOUPDATE env var
|
||||
- 2019-08-22 AUTH-2018: Adds support for authorized keys and short lived certs
|
||||
- 2019-08-28 AUTH-2022: Adds ssh timeout configuration
|
||||
- 2019-08-28 TUN-1968: Gracefully diff StreamHandler.UpdateConfig
|
||||
- 2019-08-26 AUTH-2021 - s3 bucket uploading for SSH logs
|
||||
- 2019-08-19 AUTH-2004: Adds static host key support
|
||||
- 2019-07-18 AUTH-1941: Adds initial SSH server implementation
|
||||
|
||||
2019.8.3
|
||||
- 2019-08-20 STOR-519: Add db-connect, a SQL over HTTPS server
|
||||
- 2019-08-20 Release 2019.8.2
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// DirectoryUploadManager is used to manage file uploads on an interval from a directory
|
||||
type DirectoryUploadManager struct {
|
||||
logger *logrus.Logger
|
||||
uploader Uploader
|
||||
rootDirectory string
|
||||
sweepInterval time.Duration
|
||||
ticker *time.Ticker
|
||||
shutdownC chan struct{}
|
||||
workQueue chan string
|
||||
}
|
||||
|
||||
// NewDirectoryUploadManager create a new DirectoryUploadManager
|
||||
// uploader is an Uploader to use as an actual uploading engine
|
||||
// directory is the directory to sweep for files to upload
|
||||
// sweepInterval is how often to iterate the directory and upload the files within
|
||||
func NewDirectoryUploadManager(logger *logrus.Logger, uploader Uploader, directory string, sweepInterval time.Duration, shutdownC chan struct{}) *DirectoryUploadManager {
|
||||
workerCount := 10
|
||||
manager := &DirectoryUploadManager{
|
||||
logger: logger,
|
||||
uploader: uploader,
|
||||
rootDirectory: directory,
|
||||
sweepInterval: sweepInterval,
|
||||
shutdownC: shutdownC,
|
||||
workQueue: make(chan string, workerCount),
|
||||
}
|
||||
|
||||
//start workers
|
||||
for i := 0; i < workerCount; i++ {
|
||||
go manager.worker()
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// Upload a file using the uploader
|
||||
// This is useful for "out of band" uploads that need to be triggered immediately instead of waiting for the sweep
|
||||
func (m *DirectoryUploadManager) Upload(filepath string) error {
|
||||
return m.uploader.Upload(filepath)
|
||||
}
|
||||
|
||||
// Start the upload ticker to walk the directories
|
||||
func (m *DirectoryUploadManager) Start() {
|
||||
m.ticker = time.NewTicker(m.sweepInterval)
|
||||
go m.run()
|
||||
}
|
||||
|
||||
func (m *DirectoryUploadManager) run() {
|
||||
for {
|
||||
select {
|
||||
case <-m.shutdownC:
|
||||
m.ticker.Stop()
|
||||
return
|
||||
case <-m.ticker.C:
|
||||
m.sweep()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sweep the directory and kick off uploads
|
||||
func (m *DirectoryUploadManager) sweep() {
|
||||
filepath.Walk(m.rootDirectory, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
//30 days ago
|
||||
retentionTime := 30 * (time.Hour * 24)
|
||||
checkTime := time.Now().Add(-time.Duration(retentionTime))
|
||||
|
||||
//delete the file it is stale
|
||||
if info.ModTime().Before(checkTime) {
|
||||
os.Remove(path)
|
||||
return nil
|
||||
}
|
||||
//add the upload to the work queue
|
||||
go func() {
|
||||
m.workQueue <- path
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// worker handles upload requests
|
||||
func (m *DirectoryUploadManager) worker() {
|
||||
for {
|
||||
select {
|
||||
case <-m.shutdownC:
|
||||
return
|
||||
case filepath := <-m.workQueue:
|
||||
if err := m.Upload(filepath); err != nil {
|
||||
m.logger.WithError(err).Error("Cannot upload file to s3 bucket")
|
||||
} else {
|
||||
os.Remove(filepath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type MockUploader struct {
|
||||
shouldFail bool
|
||||
}
|
||||
|
||||
func (m *MockUploader) Upload(filepath string) error {
|
||||
if m.shouldFail {
|
||||
return errors.New("upload set to fail")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMockUploader(shouldFail bool) Uploader {
|
||||
return &MockUploader{shouldFail: shouldFail}
|
||||
}
|
||||
|
||||
func getDirectoryPath(t *testing.T) string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the test directory!", err)
|
||||
}
|
||||
return filepath.Join(dir, "uploads")
|
||||
}
|
||||
|
||||
func setupTestDirectory(t *testing.T) string {
|
||||
path := getDirectoryPath(t)
|
||||
os.RemoveAll(path)
|
||||
time.Sleep(100 * time.Millisecond) //short way to wait for the OS to delete the folder
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the test directory!", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func createUploadManager(t *testing.T, shouldFailUpload bool) *DirectoryUploadManager {
|
||||
rootDirectory := setupTestDirectory(t)
|
||||
uploader := NewMockUploader(shouldFailUpload)
|
||||
logger := logrus.New()
|
||||
shutdownC := make(chan struct{})
|
||||
return NewDirectoryUploadManager(logger, uploader, rootDirectory, 1*time.Second, shutdownC)
|
||||
}
|
||||
|
||||
func createFile(t *testing.T, fileName string) (*os.File, string) {
|
||||
path := filepath.Join(getDirectoryPath(t), fileName)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatal("upload to create file for sweep test", err)
|
||||
}
|
||||
return f, path
|
||||
}
|
||||
|
||||
func TestUploadSuccess(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
path := filepath.Join(getDirectoryPath(t), "test_file")
|
||||
if err := manager.Upload(path); err != nil {
|
||||
t.Fatal("the upload request method failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadFailure(t *testing.T) {
|
||||
manager := createUploadManager(t, true)
|
||||
path := filepath.Join(getDirectoryPath(t), "test_file")
|
||||
if err := manager.Upload(path); err == nil {
|
||||
t.Fatal("the upload request method should have failed and didn't", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweepSuccess(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
f, path := createFile(t, "test_file")
|
||||
defer f.Close()
|
||||
|
||||
manager.Start()
|
||||
time.Sleep(2 * time.Second)
|
||||
if _, err := os.Stat(path); os.IsExist(err) {
|
||||
//the file should have been deleted
|
||||
t.Fatal("the manager failed to delete the file", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweepFailure(t *testing.T) {
|
||||
manager := createUploadManager(t, true)
|
||||
f, path := createFile(t, "test_file")
|
||||
defer f.Close()
|
||||
|
||||
manager.Start()
|
||||
time.Sleep(2 * time.Second)
|
||||
_, serr := f.Stat()
|
||||
if serr != nil {
|
||||
//the file should still exist
|
||||
os.Remove(path)
|
||||
t.Fatal("the manager failed to delete the file", serr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHighLoad(t *testing.T) {
|
||||
manager := createUploadManager(t, false)
|
||||
for i := 0; i < 30; i++ {
|
||||
f, _ := createFile(t, randomString(6))
|
||||
defer f.Close()
|
||||
}
|
||||
manager.Start()
|
||||
time.Sleep(4 * time.Second)
|
||||
|
||||
directory := getDirectoryPath(t)
|
||||
files, err := ioutil.ReadDir(directory)
|
||||
if err != nil || len(files) > 0 {
|
||||
t.Fatalf("the manager failed to upload all the files: %s files left: %d", err, len(files))
|
||||
}
|
||||
}
|
||||
|
||||
// LowerCase [a-z]
|
||||
const randSet = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
// String returns a string of length 'n' from a set of letters 'lset'
|
||||
func randomString(n int) string {
|
||||
b := make([]byte, n)
|
||||
lsetLen := len(randSet)
|
||||
for i := range b {
|
||||
b[i] = randSet[rand.Intn(lsetLen)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package awsuploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
//FileUploader aws compliant bucket upload
|
||||
type FileUploader struct {
|
||||
storage *s3.S3
|
||||
bucketName string
|
||||
clientID string
|
||||
secretID string
|
||||
}
|
||||
|
||||
// NewFileUploader creates a new S3 compliant bucket uploader
|
||||
func NewFileUploader(bucketName, region, accessKeyID, secretID, token, s3Host string) (*FileUploader, error) {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(region),
|
||||
Credentials: credentials.NewStaticCredentials(accessKeyID, secretID, token),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var storage *s3.S3
|
||||
if s3Host != "" {
|
||||
storage = s3.New(sess, &aws.Config{Endpoint: aws.String(s3Host)})
|
||||
} else {
|
||||
storage = s3.New(sess)
|
||||
}
|
||||
|
||||
return &FileUploader{
|
||||
storage: storage,
|
||||
bucketName: bucketName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Upload a file to the bucket
|
||||
func (u *FileUploader) Upload(filepath string) error {
|
||||
info, err := os.Stat(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, serr := u.storage.PutObjectWithContext(context.Background(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(u.bucketName),
|
||||
Key: aws.String(info.Name()),
|
||||
Body: file,
|
||||
})
|
||||
return serr
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package awsuploader
|
||||
|
||||
// UploadManager is used to manage file uploads on an interval
|
||||
type UploadManager interface {
|
||||
Upload(string) error
|
||||
Start()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package awsuploader
|
||||
|
||||
// Uploader the functions required to upload to a bucket
|
||||
type Uploader interface {
|
||||
//Upload a file to the bucket
|
||||
Upload(string) error
|
||||
}
|
||||
+7
-7
@@ -114,7 +114,7 @@ func createWebsocketStream(options *StartOptions) (*cloudflaredWebsocket.Conn, e
|
||||
|
||||
wsConn, resp, err := cloudflaredWebsocket.ClientConnect(req, nil)
|
||||
defer closeRespBody(resp)
|
||||
if err != nil && isAccessResponse(resp) {
|
||||
if err != nil && IsAccessResponse(resp) {
|
||||
wsConn, err = createAccessAuthenticatedStream(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -126,10 +126,10 @@ func createWebsocketStream(options *StartOptions) (*cloudflaredWebsocket.Conn, e
|
||||
return &cloudflaredWebsocket.Conn{Conn: wsConn}, nil
|
||||
}
|
||||
|
||||
// isAccessResponse checks the http Response to see if the url location
|
||||
// IsAccessResponse checks the http Response to see if the url location
|
||||
// contains the Access structure.
|
||||
func isAccessResponse(resp *http.Response) bool {
|
||||
if resp == nil || resp.StatusCode <= 300 {
|
||||
func IsAccessResponse(resp *http.Response) bool {
|
||||
if resp == nil || resp.StatusCode != http.StatusFound {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ func createAccessAuthenticatedStream(options *StartOptions) (*websocket.Conn, er
|
||||
return wsConn, nil
|
||||
}
|
||||
|
||||
if !isAccessResponse(resp) {
|
||||
if !IsAccessResponse(resp) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func createAccessAuthenticatedStream(options *StartOptions) (*websocket.Conn, er
|
||||
|
||||
// createAccessWebSocketStream builds an Access request and makes a connection
|
||||
func createAccessWebSocketStream(options *StartOptions) (*websocket.Conn, *http.Response, error) {
|
||||
req, err := buildAccessRequest(options)
|
||||
req, err := BuildAccessRequest(options)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -187,7 +187,7 @@ func createAccessWebSocketStream(options *StartOptions) (*websocket.Conn, *http.
|
||||
}
|
||||
|
||||
// buildAccessRequest builds an HTTP request with the Access token set
|
||||
func buildAccessRequest(options *StartOptions) (*http.Request, error) {
|
||||
func BuildAccessRequest(options *StartOptions) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -102,14 +102,14 @@ func TestIsAccessResponse(t *testing.T) {
|
||||
ExpectedOut bool
|
||||
}{
|
||||
{"nil response", nil, false},
|
||||
{"redirect with no location", &http.Response{StatusCode: http.StatusPermanentRedirect}, false},
|
||||
{"redirect with no location", &http.Response{StatusCode: http.StatusFound}, false},
|
||||
{"200 ok", &http.Response{StatusCode: http.StatusOK}, false},
|
||||
{"redirect with location", &http.Response{StatusCode: http.StatusPermanentRedirect, Header: validLocationHeader}, true},
|
||||
{"redirect with invalid location", &http.Response{StatusCode: http.StatusPermanentRedirect, Header: invalidLocationHeader}, false},
|
||||
{"redirect with location", &http.Response{StatusCode: http.StatusFound, Header: validLocationHeader}, true},
|
||||
{"redirect with invalid location", &http.Response{StatusCode: http.StatusFound, Header: invalidLocationHeader}, false},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
if isAccessResponse(tc.In) != tc.ExpectedOut {
|
||||
if IsAccessResponse(tc.In) != tc.ExpectedOut {
|
||||
t.Fatalf("Failed case %d -- %s", i, tc.Description)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,14 +93,3 @@ stretch: &stretch
|
||||
- make test
|
||||
|
||||
jessie: *stretch
|
||||
|
||||
# cfsetup compose
|
||||
default-stack: test_dbconnect
|
||||
test_dbconnect:
|
||||
compose:
|
||||
up-args:
|
||||
- --renew-anon-volumes
|
||||
- --abort-on-container-exit
|
||||
- --exit-code-from=cloudflared
|
||||
files:
|
||||
- dbconnect/integration_test/dbconnect.yaml
|
||||
@@ -1,17 +1,20 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/sshgen"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
@@ -188,9 +191,14 @@ func login(c *cli.Context) error {
|
||||
logger.Errorf("Please provide the url of the Access application\n")
|
||||
return err
|
||||
}
|
||||
token, err := token.FetchToken(appURL)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to fetch token: %s\n", err)
|
||||
if err := verifyTokenAtEdge(appURL, c); err != nil {
|
||||
logger.WithError(err).Error("Could not verify token")
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := token.GetTokenIfExists(appURL)
|
||||
if err != nil || token == "" {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", string(token))
|
||||
@@ -372,3 +380,59 @@ func isFileThere(candidate string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// verifyTokenAtEdge checks for a token on disk, or generates a new one.
|
||||
// Then makes a request to to the origin with the token to ensure it is valid.
|
||||
// Returns nil if token is valid.
|
||||
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
|
||||
}
|
||||
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers}
|
||||
|
||||
if valid, err := isTokenValid(options); err != nil {
|
||||
return err
|
||||
} else if valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := token.RemoveTokenIfExists(appUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if valid, err := isTokenValid(options); err != nil {
|
||||
return err
|
||||
} else if !valid {
|
||||
return errors.New("failed to verify token")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isTokenValid makes a request to the origin and returns true if the response was not a 302.
|
||||
func isTokenValid(options *carrier.StartOptions) (bool, error) {
|
||||
req, err := carrier.BuildAccessRequest(options)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Could not create access request")
|
||||
}
|
||||
|
||||
// Do not follow redirects
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Timeout: time.Second * 5,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// A redirect to login means the token was invalid.
|
||||
return !carrier.IsAccessResponse(resp), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//+build darwin
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return exec.Command("open", url)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//+build !windows,!darwin,!linux,!netbsd,!freebsd,!openbsd
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//+build linux freebsd openbsd netbsd
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return exec.Command("xdg-open", url)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//+build windows
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
cmd := exec.Command("cmd")
|
||||
// CmdLine is only defined when compiling for windows.
|
||||
// Empty string is the cmd proc "Title". Needs to be included because the start command will interpret the first
|
||||
// quoted string as that field and we want to quote the URL.
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: fmt.Sprintf(`/c start "" "%s"`, url)}
|
||||
return cmd
|
||||
}
|
||||
@@ -4,25 +4,11 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// OpenBrowser opens the specified URL in the default browser of the user
|
||||
func OpenBrowser(url string) error {
|
||||
var cmd string
|
||||
var args []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = "cmd"
|
||||
args = []string{"/c", "start"}
|
||||
case "darwin":
|
||||
cmd = "open"
|
||||
default: // "linux", "freebsd", "openbsd", "netbsd"
|
||||
cmd = "xdg-open"
|
||||
}
|
||||
args = append(args, url)
|
||||
return exec.Command(cmd, args...).Start()
|
||||
return getBrowserCmd(url).Start()
|
||||
}
|
||||
|
||||
// Run will kick off a shell task and pipe the results to the respective std pipes
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"gopkg.in/coreos/go-oidc.v1/jose"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,15 +26,15 @@ var logger = log.CreateLogger()
|
||||
type lock struct {
|
||||
lockFilePath string
|
||||
backoff *origin.BackoffHandler
|
||||
sigHandler *signalHandler
|
||||
sigHandler *signalHandler
|
||||
}
|
||||
|
||||
type signalHandler struct {
|
||||
sigChannel chan os.Signal
|
||||
signals []os.Signal
|
||||
sigChannel chan os.Signal
|
||||
signals []os.Signal
|
||||
}
|
||||
|
||||
func (s *signalHandler) register(handler func()) {
|
||||
func (s *signalHandler) register(handler func()){
|
||||
s.sigChannel = make(chan os.Signal, 1)
|
||||
signal.Notify(s.sigChannel, s.signals...)
|
||||
go func(s *signalHandler) {
|
||||
@@ -59,8 +59,8 @@ func newLock(path string) *lock {
|
||||
return &lock{
|
||||
lockFilePath: lockPath,
|
||||
backoff: &origin.BackoffHandler{MaxRetries: 7},
|
||||
sigHandler: &signalHandler{
|
||||
signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
|
||||
sigHandler: &signalHandler{
|
||||
signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -68,8 +68,8 @@ func newLock(path string) *lock {
|
||||
func (l *lock) Acquire() error {
|
||||
// Intercept SIGINT and SIGTERM to release lock before exiting
|
||||
l.sigHandler.register(func() {
|
||||
l.deleteLockFile()
|
||||
os.Exit(0)
|
||||
l.deleteLockFile()
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
// Check for a path.lock file
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
baseStoreURL = "https://login.cloudflarewarp.com/"
|
||||
baseStoreURL = "https://login.argotunnel.com/"
|
||||
clientTimeout = time.Second * 60
|
||||
)
|
||||
|
||||
|
||||
+226
-89
@@ -7,38 +7,78 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/trace"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/dbconnect"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/supervisor"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
|
||||
"github.com/cloudflare/cloudflared/awsuploader"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/cmd/sqlgateway"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/sshlog"
|
||||
"github.com/cloudflare/cloudflared/sshserver"
|
||||
"github.com/cloudflare/cloudflared/supervisor"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
)
|
||||
|
||||
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
|
||||
const (
|
||||
sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
|
||||
|
||||
sshLogFileDirectory = "/var/log/cloudflared/"
|
||||
|
||||
// sshPortFlag is the port on localhost the cloudflared ssh server will run on
|
||||
sshPortFlag = "local-ssh-port"
|
||||
|
||||
// sshIdleTimeoutFlag defines the duration a SSH session can remain idle before being closed
|
||||
sshIdleTimeoutFlag = "ssh-idle-timeout"
|
||||
|
||||
// sshMaxTimeoutFlag defines the max duration a SSH session can remain open for
|
||||
sshMaxTimeoutFlag = "ssh-max-timeout"
|
||||
|
||||
// bucketNameFlag is the bucket name to use for the SSH log uploader
|
||||
bucketNameFlag = "bucket-name"
|
||||
|
||||
// regionNameFlag is the AWS region name to use for the SSH log uploader
|
||||
regionNameFlag = "region-name"
|
||||
|
||||
// secretIDFlag is the Secret id of SSH log uploader
|
||||
secretIDFlag = "secret-id"
|
||||
|
||||
// accessKeyIDFlag is the Access key id of SSH log uploader
|
||||
accessKeyIDFlag = "access-key-id"
|
||||
|
||||
// sessionTokenIDFlag is the Session token of SSH log uploader
|
||||
sessionTokenIDFlag = "session-token"
|
||||
|
||||
// s3URLFlag is the S3 URL of SSH log uploader (e.g. don't use AWS s3 and use google storage bucket instead)
|
||||
s3URLFlag = "s3-url-host"
|
||||
|
||||
// disablePortForwarding disables both remote and local ssh port forwarding
|
||||
enablePortForwardingFlag = "enable-port-forwarding"
|
||||
|
||||
noIntentMsg = "The --intent argument is required. Cloudflared looks up an Intent to determine what configuration to use (i.e. which tunnels to start). If you don't have any Intents yet, you can use a placeholder Intent Label for now. Then, when you make an Intent with that label, cloudflared will get notified and open the tunnels you specified in that Intent."
|
||||
)
|
||||
|
||||
var (
|
||||
shutdownC chan struct{}
|
||||
@@ -98,7 +138,43 @@ func Commands() []*cli.Command {
|
||||
ArgsUsage: " ", // can't be the empty string or we get the default output
|
||||
Hidden: false,
|
||||
},
|
||||
dbConnectCmd(),
|
||||
{
|
||||
Name: "db",
|
||||
Action: func(c *cli.Context) error {
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
|
||||
fmt.Printf("\nSQL Database Password: ")
|
||||
pass, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
go sqlgateway.StartProxy(c, logger, string(pass))
|
||||
|
||||
raven.CapturePanic(func() { err = tunnel(c) }, nil)
|
||||
if err != nil {
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
return err
|
||||
},
|
||||
Before: Before,
|
||||
Usage: "SQL Gateway is an SQL over HTTP reverse proxy",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "db",
|
||||
Value: true,
|
||||
Usage: "Enable the SQL Gateway Proxy",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Value: "",
|
||||
Usage: "Database connection string: db://user:pass",
|
||||
},
|
||||
},
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
var subcommands []*cli.Command
|
||||
@@ -290,6 +366,57 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
c.Set("url", "https://"+helloListener.Addr().String())
|
||||
}
|
||||
|
||||
if c.IsSet("ssh-server") {
|
||||
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
|
||||
msg := fmt.Sprintf("--ssh-server is not supported on %s", runtime.GOOS)
|
||||
logger.Error(msg)
|
||||
return errors.New(msg)
|
||||
|
||||
}
|
||||
|
||||
logger.Infof("ssh-server set")
|
||||
|
||||
logManager := sshlog.NewEmptyManager()
|
||||
if c.IsSet(bucketNameFlag) && c.IsSet(regionNameFlag) && c.IsSet(accessKeyIDFlag) && c.IsSet(secretIDFlag) {
|
||||
uploader, err := awsuploader.NewFileUploader(c.String(bucketNameFlag), c.String(regionNameFlag),
|
||||
c.String(accessKeyIDFlag), c.String(secretIDFlag), c.String(sessionTokenIDFlag), c.String(s3URLFlag))
|
||||
if err != nil {
|
||||
msg := "Cannot create uploader for SSH Server"
|
||||
logger.WithError(err).Error(msg)
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(sshLogFileDirectory, 0600); err != nil {
|
||||
msg := fmt.Sprintf("Cannot create SSH log file directory %s", sshLogFileDirectory)
|
||||
logger.WithError(err).Errorf(msg)
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
|
||||
logManager = sshlog.New(sshLogFileDirectory)
|
||||
|
||||
uploadManager := awsuploader.NewDirectoryUploadManager(logger, uploader, sshLogFileDirectory, 30*time.Minute, shutdownC)
|
||||
uploadManager.Start()
|
||||
}
|
||||
|
||||
sshServerAddress := "127.0.0.1:" + c.String(sshPortFlag)
|
||||
server, err := sshserver.New(logManager, logger, version, sshServerAddress, shutdownC, c.Duration(sshIdleTimeoutFlag), c.Duration(sshMaxTimeoutFlag), c.Bool(enablePortForwardingFlag))
|
||||
if err != nil {
|
||||
msg := "Cannot create new SSH Server"
|
||||
logger.WithError(err).Error(msg)
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err = server.Start(); err != nil && err != ssh.ErrServerClosed {
|
||||
logger.WithError(err).Error("SSH server error")
|
||||
}
|
||||
// TODO: remove when declarative tunnels are implemented.
|
||||
close(shutdownC)
|
||||
}()
|
||||
c.Set("url", "ssh://"+sshServerAddress)
|
||||
}
|
||||
|
||||
if host := hostnameFromURI(c.String("url")); host != "" {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:")
|
||||
if err != nil {
|
||||
@@ -395,21 +522,16 @@ func startDeclarativeTunnel(ctx context.Context,
|
||||
return err
|
||||
}
|
||||
|
||||
var scope pogs.Scope
|
||||
if c.IsSet("group") == c.IsSet("system-name") {
|
||||
err = fmt.Errorf("exactly one of --group or --system-name must be specified")
|
||||
logger.WithError(err).Error("unable to determine scope")
|
||||
return err
|
||||
} else if c.IsSet("group") {
|
||||
scope = pogs.NewGroup(c.String("group"))
|
||||
} else {
|
||||
scope = pogs.NewSystemName(c.String("system-name"))
|
||||
intentLabel := c.String("intent")
|
||||
if intentLabel == "" {
|
||||
logger.Error("--intent was empty")
|
||||
return fmt.Errorf(noIntentMsg)
|
||||
}
|
||||
|
||||
cloudflaredConfig := &connection.CloudflaredConfig{
|
||||
BuildInfo: buildInfo,
|
||||
CloudflaredID: cloudflaredID,
|
||||
Scope: scope,
|
||||
IntentLabel: intentLabel,
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
@@ -522,60 +644,6 @@ func addPortIfMissing(uri *url.URL, port int) string {
|
||||
return fmt.Sprintf("%s:%d", uri.Hostname(), port)
|
||||
}
|
||||
|
||||
func dbConnectCmd() *cli.Command {
|
||||
cmd := dbconnect.Cmd()
|
||||
|
||||
// Append the tunnel commands so users can customize the daemon settings.
|
||||
cmd.Flags = appendFlags(Flags(), cmd.Flags...)
|
||||
|
||||
// Override before to run tunnel validation before dbconnect validation.
|
||||
cmd.Before = func(c *cli.Context) error {
|
||||
err := Before(c)
|
||||
if err == nil {
|
||||
err = dbconnect.CmdBefore(c)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Override action to setup the Proxy, then if successful, start the tunnel daemon.
|
||||
cmd.Action = func(c *cli.Context) error {
|
||||
err := dbconnect.CmdAction(c)
|
||||
if err == nil {
|
||||
err = tunnel(c)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// appendFlags will append extra flags to a slice of flags.
|
||||
//
|
||||
// The cli package will panic if two flags exist with the same name,
|
||||
// so if extraFlags contains a flag that was already defined, modify the
|
||||
// original flags to use the extra version.
|
||||
func appendFlags(flags []cli.Flag, extraFlags ...cli.Flag) []cli.Flag {
|
||||
for _, extra := range extraFlags {
|
||||
var found bool
|
||||
|
||||
// Check if an extra flag overrides an existing flag.
|
||||
for i, flag := range flags {
|
||||
if reflect.DeepEqual(extra.Names(), flag.Names()) {
|
||||
flags[i] = extra
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Append the extra flag if it has nothing to override.
|
||||
if !found {
|
||||
flags = append(flags, extra)
|
||||
}
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
@@ -591,10 +659,11 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "no-autoupdate",
|
||||
Usage: "Disable periodic check for updates, restarting the server with the new version.",
|
||||
Value: false,
|
||||
Hidden: shouldHide,
|
||||
Name: "no-autoupdate",
|
||||
Usage: "Disable periodic check for updates, restarting the server with the new version.",
|
||||
EnvVars: []string{"NO_AUTOUPDATE"},
|
||||
Value: false,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "is-autoupdated",
|
||||
@@ -652,6 +721,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_HOSTNAME"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "http-host-header",
|
||||
Usage: "Sets the HTTP Host header for the local webserver.",
|
||||
EnvVars: []string{"TUNNEL_HTTP_HOST_HEADER"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "origin-server-name",
|
||||
Usage: "Hostname on the origin server certificate.",
|
||||
@@ -749,6 +824,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "ssh-server",
|
||||
Value: false,
|
||||
Usage: "Run an SSH Server",
|
||||
EnvVars: []string{"TUNNEL_SSH_SERVER"},
|
||||
Hidden: true, // TODO: remove when feature is complete
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "pidfile",
|
||||
Usage: "Write the application's PID to this file after first successful connection.",
|
||||
@@ -873,15 +955,9 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "system-name",
|
||||
Usage: "Unique identifier for this cloudflared instance. It can be configured individually in the Declarative Tunnel UI. Mutually exclusive with `--group`.",
|
||||
EnvVars: []string{"TUNNEL_SYSTEM_NAME"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "group",
|
||||
Usage: "Name of a group of cloudflared instances, of which this instance should be an identical copy. They can be configured collectively in the Declarative Tunnel UI. Mutually exclusive with `--system-name`.",
|
||||
EnvVars: []string{"TUNNEL_GROUP"},
|
||||
Name: "intent",
|
||||
Usage: "The label of an Intent from which `cloudflared` should gets its tunnels from. Intents can be created in the Origin Registry UI.",
|
||||
EnvVars: []string{"TUNNEL_INTENT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
@@ -891,5 +967,66 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"DIAL_EDGE_TIMEOUT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: sshPortFlag,
|
||||
Usage: "Localhost port that cloudflared SSH server will run on",
|
||||
Value: "2222",
|
||||
EnvVars: []string{"LOCAL_SSH_PORT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: sshIdleTimeoutFlag,
|
||||
Usage: "Connection timeout after no activity",
|
||||
EnvVars: []string{"SSH_IDLE_TIMEOUT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: sshMaxTimeoutFlag,
|
||||
Usage: "Absolute connection timeout",
|
||||
EnvVars: []string{"SSH_MAX_TIMEOUT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: bucketNameFlag,
|
||||
Usage: "Bucket name of where to upload SSH logs",
|
||||
EnvVars: []string{"BUCKET_ID"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: regionNameFlag,
|
||||
Usage: "Region name of where to upload SSH logs",
|
||||
EnvVars: []string{"REGION_ID"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: accessKeyIDFlag,
|
||||
Usage: "Access Key ID of where to upload SSH logs",
|
||||
EnvVars: []string{"ACCESS_CLIENT_ID"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: secretIDFlag,
|
||||
Usage: "Secret ID of where to upload SSH logs",
|
||||
EnvVars: []string{"SECRET_ID"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: sessionTokenIDFlag,
|
||||
Usage: "Session Token to use in the configuration of SSH logs uploading",
|
||||
EnvVars: []string{"SESSION_TOKEN_ID"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: s3URLFlag,
|
||||
Usage: "S3 url of where to upload SSH logs",
|
||||
EnvVars: []string{"S3_URL"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: enablePortForwardingFlag,
|
||||
Usage: "Enables remote and local SSH port forwarding",
|
||||
EnvVars: []string{"ENABLE_PORT_FORWARDING"},
|
||||
Hidden: true,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +253,7 @@ func prepareTunnelConfig(
|
||||
HTTPTransport: httpTransport,
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
Hostname: hostname,
|
||||
HTTPHostHeader: c.String("http-host-header"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
IsFreeTunnel: isFreeTunnel,
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
const (
|
||||
baseLoginURL = "https://dash.cloudflare.com/argotunnel"
|
||||
callbackStoreURL = "https://login.cloudflarewarp.com/"
|
||||
callbackStoreURL = "https://login.argotunnel.com/"
|
||||
)
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package sqlgateway
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/elgs/gosqljson"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Connection Connection `json:"connection"`
|
||||
Command string `json:"command"`
|
||||
Params []interface{} `json:"params"`
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
SSLMode string `json:"sslmode"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Columns []string `json:"columns"`
|
||||
Rows [][]string `json:"rows"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
Context *cli.Context
|
||||
Router *mux.Router
|
||||
Token string
|
||||
User string
|
||||
Password string
|
||||
Driver string
|
||||
Database string
|
||||
Logger *logrus.Logger
|
||||
}
|
||||
|
||||
func StartProxy(c *cli.Context, logger *logrus.Logger, password string) error {
|
||||
proxy := NewProxy(c, logger, password)
|
||||
|
||||
logger.Infof("Starting SQL Gateway Proxy on port %s", strings.Split(c.String("url"), ":")[1])
|
||||
|
||||
err := http.ListenAndServe(":"+strings.Split(c.String("url"), ":")[1], proxy.Router)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func randID(n int, c *cli.Context) string {
|
||||
charBytes := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = charBytes[rand.Intn(len(charBytes))]
|
||||
}
|
||||
return fmt.Sprintf("%s&%s", c.String("hostname"), b)
|
||||
}
|
||||
|
||||
// db://user@dbname
|
||||
func parseInfo(input string) (string, string, string) {
|
||||
p1 := strings.Split(input, "://")
|
||||
p2 := strings.Split(p1[1], "@")
|
||||
return p1[0], p2[0], p2[1]
|
||||
}
|
||||
|
||||
func NewProxy(c *cli.Context, logger *logrus.Logger, pass string) *Proxy {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
driver, user, dbname := parseInfo(c.String("address"))
|
||||
proxy := Proxy{
|
||||
Context: c,
|
||||
Router: mux.NewRouter(),
|
||||
Token: randID(64, c),
|
||||
Logger: logger,
|
||||
User: user,
|
||||
Password: pass,
|
||||
Database: dbname,
|
||||
Driver: driver,
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf(`
|
||||
|
||||
--------------------
|
||||
SQL Gateway Proxy
|
||||
Token: %s
|
||||
--------------------
|
||||
|
||||
`, proxy.Token))
|
||||
|
||||
proxy.Router.HandleFunc("/", proxy.proxyRequest).Methods("POST")
|
||||
return &proxy
|
||||
}
|
||||
|
||||
func (proxy *Proxy) proxyRequest(rw http.ResponseWriter, req *http.Request) {
|
||||
var message Message
|
||||
response := Response{}
|
||||
|
||||
err := json.NewDecoder(req.Body).Decode(&message)
|
||||
if err != nil {
|
||||
proxy.Logger.Error(err)
|
||||
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if message.Connection.Token != proxy.Token {
|
||||
proxy.Logger.Error("Invalid token")
|
||||
http.Error(rw, "400 - Invalid token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
connStr := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=%s", proxy.User, proxy.Password, proxy.Database, message.Connection.SSLMode)
|
||||
|
||||
db, err := sql.Open(proxy.Driver, connStr)
|
||||
defer db.Close()
|
||||
|
||||
if err != nil {
|
||||
proxy.Logger.Error(err)
|
||||
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
|
||||
} else {
|
||||
proxy.Logger.Info("Forwarding SQL: ", message.Command)
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
||||
headers, data, err := gosqljson.QueryDbToArray(db, "lower", message.Command, message.Params...)
|
||||
|
||||
if err != nil {
|
||||
proxy.Logger.Error(err)
|
||||
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
|
||||
} else {
|
||||
response = Response{headers, data, ""}
|
||||
}
|
||||
}
|
||||
json.NewEncoder(rw).Encode(response)
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func (c *Connection) Serve(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Connect is used to establish connections with cloudflare's edge network
|
||||
func (c *Connection) Connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters, logger *logrus.Entry) (*pogs.ConnectResult, error) {
|
||||
func (c *Connection) Connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters, logger *logrus.Entry) (pogs.ConnectResult, error) {
|
||||
openStreamCtx, cancel := context.WithTimeout(ctx, openStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// Used to discover HA Warp servers
|
||||
srvService = "warp"
|
||||
// Used to discover HA origintunneld servers
|
||||
srvService = "origintunneld"
|
||||
srvProto = "tcp"
|
||||
srvName = "cloudflarewarp.com"
|
||||
srvName = "argotunnel.com"
|
||||
|
||||
// Used to fallback to DoT when we can't use the default resolver to
|
||||
// discover HA Warp servers (GitHub issue #75).
|
||||
// discover HA origintunneld servers (GitHub issue #75).
|
||||
dotServerName = "cloudflare-dns.com"
|
||||
dotServerAddr = "1.1.1.1:853"
|
||||
dotTimeout = time.Duration(15 * time.Second)
|
||||
@@ -30,8 +30,8 @@ const (
|
||||
|
||||
var friendlyDNSErrorLines = []string{
|
||||
`Please try the following things to diagnose this issue:`,
|
||||
` 1. ensure that cloudflarewarp.com is returning "warp" service records.`,
|
||||
` Run your system's equivalent of: dig srv _warp._tcp.cloudflarewarp.com`,
|
||||
` 1. ensure that argotunnel.com is returning "origintunneld" service records.`,
|
||||
` Run your system's equivalent of: dig srv _origintunneld._tcp.argotunnel.com`,
|
||||
` 2. ensure that your DNS resolver is not returning compressed SRV records.`,
|
||||
` See GitHub issue https://github.com/golang/go/issues/27546`,
|
||||
` For example, you could use Cloudflare's 1.1.1.1 as your resolver:`,
|
||||
@@ -102,7 +102,7 @@ func EdgeDiscovery(logger *logrus.Entry) ([]*net.TCPAddr, error) {
|
||||
// Try to fall back to DoT from Cloudflare directly.
|
||||
//
|
||||
// Note: Instead of DoT, we could also have used DoH. Either of these:
|
||||
// - directly via the JSON API (https://1.1.1.1/dns-query?ct=application/dns-json&name=_warp._tcp.cloudflarewarp.com&type=srv)
|
||||
// - directly via the JSON API (https://1.1.1.1/dns-query?ct=application/dns-json&name=_origintunneld._tcp.argotunnel.com&type=srv)
|
||||
// - indirectly via `tunneldns.NewUpstreamHTTPS()`
|
||||
// But both of these cases miss out on a key feature from the stdlib:
|
||||
// "The returned records are sorted by priority and randomized by weight within a priority."
|
||||
@@ -119,7 +119,7 @@ func EdgeDiscovery(logger *logrus.Entry) ([]*net.TCPAddr, error) {
|
||||
for _, s := range friendlyDNSErrorLines {
|
||||
logger.Errorln(s)
|
||||
}
|
||||
return nil, errors.Wrap(err, "Could not lookup srv records on _warp._tcp.cloudflarewarp.com")
|
||||
return nil, errors.Wrap(err, "Could not lookup srv records on _origintunneld._tcp.argotunnel.com")
|
||||
}
|
||||
// Accept the fallback results and keep going
|
||||
addrs = fallbackAddrs
|
||||
|
||||
+27
-17
@@ -17,8 +17,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
quickStartLink = "https://developers.cloudflare.com/argo-tunnel/quickstart/"
|
||||
faqLink = "https://developers.cloudflare.com/argo-tunnel/faq/"
|
||||
quickStartLink = "https://developers.cloudflare.com/argo-tunnel/quickstart/"
|
||||
faqLink = "https://developers.cloudflare.com/argo-tunnel/faq/"
|
||||
defaultRetryAfter = time.Second * 5
|
||||
)
|
||||
|
||||
// EdgeManager manages connections with the edge
|
||||
@@ -47,7 +48,7 @@ type CloudflaredConfig struct {
|
||||
CloudflaredID uuid.UUID
|
||||
Tags []pogs.Tag
|
||||
BuildInfo *buildinfo.BuildInfo
|
||||
Scope pogs.Scope
|
||||
IntentLabel string
|
||||
}
|
||||
|
||||
func NewEdgeManager(
|
||||
@@ -87,8 +88,12 @@ func (em *EdgeManager) Run(ctx context.Context) error {
|
||||
// Create/delete connection one at a time, so we don't need to adjust for connections that are being created/deleted
|
||||
// in shouldCreateConnection or shouldReduceConnection calculation
|
||||
if em.state.shouldCreateConnection(em.serviceDiscoverer.AvailableAddrs()) {
|
||||
if err := em.newConnection(ctx); err != nil {
|
||||
em.logger.WithError(err).Error("cannot create new connection")
|
||||
if connErr := em.newConnection(ctx); connErr != nil {
|
||||
if !connErr.ShouldRetry {
|
||||
em.logger.WithError(connErr).Error(em.noRetryMessage())
|
||||
return connErr
|
||||
}
|
||||
em.logger.WithError(connErr).Error("cannot create new connection")
|
||||
}
|
||||
} else if em.state.shouldReduceConnection() {
|
||||
if err := em.closeConnection(ctx); err != nil {
|
||||
@@ -103,11 +108,11 @@ func (em *EdgeManager) UpdateConfigurable(newConfigurable *EdgeManagerConfigurab
|
||||
em.state.updateConfigurable(newConfigurable)
|
||||
}
|
||||
|
||||
func (em *EdgeManager) newConnection(ctx context.Context) error {
|
||||
func (em *EdgeManager) newConnection(ctx context.Context) *pogs.ConnectError {
|
||||
edgeIP := em.serviceDiscoverer.Addr()
|
||||
edgeConn, err := em.dialEdge(ctx, edgeIP)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "dial edge error")
|
||||
return retryConnection(fmt.Sprintf("dial edge error: %v", err))
|
||||
}
|
||||
configurable := em.state.getConfigurable()
|
||||
// Establish a muxed connection with the edge
|
||||
@@ -121,12 +126,12 @@ func (em *EdgeManager) newConnection(ctx context.Context) error {
|
||||
Logger: em.logger.WithField("subsystem", "muxer"),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "couldn't perform handshake with edge")
|
||||
retryConnection(fmt.Sprintf("couldn't perform handshake with edge: %v", err))
|
||||
}
|
||||
|
||||
h2muxConn, err := newConnection(muxer, edgeIP)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "couldn't create h2mux connection")
|
||||
return retryConnection(fmt.Sprintf("couldn't create h2mux connection: %v", err))
|
||||
}
|
||||
|
||||
go em.serveConn(ctx, h2muxConn)
|
||||
@@ -136,23 +141,20 @@ func (em *EdgeManager) newConnection(ctx context.Context) error {
|
||||
CloudflaredVersion: em.cloudflaredConfig.BuildInfo.CloudflaredVersion,
|
||||
NumPreviousAttempts: 0,
|
||||
OriginCert: em.state.getUserCredential(),
|
||||
Scope: em.cloudflaredConfig.Scope,
|
||||
IntentLabel: em.cloudflaredConfig.IntentLabel,
|
||||
Tags: em.cloudflaredConfig.Tags,
|
||||
}, em.logger)
|
||||
if err != nil {
|
||||
h2muxConn.Shutdown()
|
||||
return errors.Wrap(err, "couldn't connect to edge")
|
||||
return retryConnection(fmt.Sprintf("couldn't connect to edge: %v", err))
|
||||
}
|
||||
|
||||
if connErr := connResult.Err; connErr != nil {
|
||||
if !connErr.ShouldRetry {
|
||||
return errors.Wrap(connErr, em.noRetryMessage())
|
||||
}
|
||||
return errors.Wrapf(connErr, "edge responded with RetryAfter=%v", connErr.RetryAfter)
|
||||
if connErr := connResult.ConnectError(); connErr != nil {
|
||||
return connErr
|
||||
}
|
||||
|
||||
em.state.newConnection(h2muxConn)
|
||||
em.logger.Infof("connected to %s", connResult.ServerInfo.LocationName)
|
||||
em.logger.Infof("connected to %s", connResult.ConnectedTo())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -282,3 +284,11 @@ func (ems *edgeManagerState) getUserCredential() []byte {
|
||||
defer ems.RUnlock()
|
||||
return ems.userCredential
|
||||
}
|
||||
|
||||
func retryConnection(cause string) *pogs.ConnectError {
|
||||
return &pogs.ConnectError{
|
||||
Cause: cause,
|
||||
RetryAfter: defaultRetryAfter,
|
||||
ShouldRetry: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Client is an interface to talk to any database.
|
||||
//
|
||||
// Currently, the only implementation is SQLClient, but its structure
|
||||
// should be designed to handle a MongoClient or RedisClient in the future.
|
||||
type Client interface {
|
||||
Ping(context.Context) error
|
||||
Submit(context.Context, *Command) (interface{}, error)
|
||||
}
|
||||
|
||||
// NewClient creates a database client based on its URL scheme.
|
||||
func NewClient(ctx context.Context, originURL *url.URL) (Client, error) {
|
||||
return NewSQLClient(ctx, originURL)
|
||||
}
|
||||
|
||||
// Command is a standard, non-vendor format for submitting database commands.
|
||||
//
|
||||
// When determining the scope of this struct, refer to the following litmus test:
|
||||
// Could this (roughly) conform to SQL, Document-based, and Key-value command formats?
|
||||
type Command struct {
|
||||
Statement string `json:"statement"`
|
||||
Arguments Arguments `json:"arguments,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Isolation string `json:"isolation,omitempty"`
|
||||
Timeout time.Duration `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
// Validate enforces the contract of Command: non empty statement (both in length and logic),
|
||||
// lowercase mode and isolation, non-zero timeout, and valid Arguments.
|
||||
func (cmd *Command) Validate() error {
|
||||
if cmd.Statement == "" {
|
||||
return fmt.Errorf("cannot provide an empty statement")
|
||||
}
|
||||
|
||||
if strings.Map(func(char rune) rune {
|
||||
if char == ';' || unicode.IsSpace(char) {
|
||||
return -1
|
||||
}
|
||||
return char
|
||||
}, cmd.Statement) == "" {
|
||||
return fmt.Errorf("cannot provide a statement with no logic: '%s'", cmd.Statement)
|
||||
}
|
||||
|
||||
cmd.Mode = strings.ToLower(cmd.Mode)
|
||||
cmd.Isolation = strings.ToLower(cmd.Isolation)
|
||||
|
||||
if cmd.Timeout.Nanoseconds() <= 0 {
|
||||
cmd.Timeout = 24 * time.Hour
|
||||
}
|
||||
|
||||
return cmd.Arguments.Validate()
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a byte representation of JSON into a Command, which is also validated.
|
||||
func (cmd *Command) UnmarshalJSON(data []byte) error {
|
||||
// Alias is required to avoid infinite recursion from the default UnmarshalJSON.
|
||||
type Alias Command
|
||||
alias := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(cmd),
|
||||
}
|
||||
|
||||
err := json.Unmarshal(data, &alias)
|
||||
if err == nil {
|
||||
err = cmd.Validate()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Arguments is a wrapper for either map-based or array-based Command arguments.
|
||||
//
|
||||
// Each field is mutually-exclusive and some Client implementations may not
|
||||
// support both fields (eg. MySQL does not accept named arguments).
|
||||
type Arguments struct {
|
||||
Named map[string]interface{}
|
||||
Positional []interface{}
|
||||
}
|
||||
|
||||
// Validate enforces the contract of Arguments: non nil, mutually exclusive, and no empty or reserved keys.
|
||||
func (args *Arguments) Validate() error {
|
||||
if args.Named == nil {
|
||||
args.Named = map[string]interface{}{}
|
||||
}
|
||||
if args.Positional == nil {
|
||||
args.Positional = []interface{}{}
|
||||
}
|
||||
|
||||
if len(args.Named) > 0 && len(args.Positional) > 0 {
|
||||
return fmt.Errorf("both named and positional arguments cannot be specified: %+v and %+v", args.Named, args.Positional)
|
||||
}
|
||||
|
||||
for key := range args.Named {
|
||||
if key == "" {
|
||||
return fmt.Errorf("named arguments cannot contain an empty key: %+v", args.Named)
|
||||
}
|
||||
if !utf8.ValidString(key) {
|
||||
return fmt.Errorf("named argument does not conform to UTF-8 encoding: %s", key)
|
||||
}
|
||||
if strings.HasPrefix(key, "_") {
|
||||
return fmt.Errorf("named argument cannot start with a reserved keyword '_': %s", key)
|
||||
}
|
||||
if unicode.IsNumber([]rune(key)[0]) {
|
||||
return fmt.Errorf("named argument cannot start with a number: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a byte representation of JSON into Arguments, which is also validated.
|
||||
func (args *Arguments) UnmarshalJSON(data []byte) error {
|
||||
var obj interface{}
|
||||
err := json.Unmarshal(data, &obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
named, ok := obj.(map[string]interface{})
|
||||
if ok {
|
||||
args.Named = named
|
||||
} else {
|
||||
positional, ok := obj.([]interface{})
|
||||
if ok {
|
||||
args.Positional = positional
|
||||
} else {
|
||||
return fmt.Errorf("arguments must either be an object {\"0\":\"val\"} or an array [\"val\"]: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return args.Validate()
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommandValidateEmpty(t *testing.T) {
|
||||
stmts := []string{
|
||||
"",
|
||||
";",
|
||||
" \n\t",
|
||||
";\n;\t;",
|
||||
}
|
||||
|
||||
for _, stmt := range stmts {
|
||||
cmd := Command{Statement: stmt}
|
||||
|
||||
assert.Error(t, cmd.Validate(), stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateMode(t *testing.T) {
|
||||
modes := []string{
|
||||
"",
|
||||
"query",
|
||||
"ExEc",
|
||||
"PREPARE",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
cmd := Command{Statement: "Ok", Mode: mode}
|
||||
|
||||
assert.NoError(t, cmd.Validate(), mode)
|
||||
assert.Equal(t, strings.ToLower(mode), cmd.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateIsolation(t *testing.T) {
|
||||
isos := []string{
|
||||
"",
|
||||
"default",
|
||||
"read_committed",
|
||||
"SNAPshot",
|
||||
}
|
||||
|
||||
for _, iso := range isos {
|
||||
cmd := Command{Statement: "Ok", Isolation: iso}
|
||||
|
||||
assert.NoError(t, cmd.Validate(), iso)
|
||||
assert.Equal(t, strings.ToLower(iso), cmd.Isolation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateTimeout(t *testing.T) {
|
||||
cmd := Command{Statement: "Ok", Timeout: 0}
|
||||
|
||||
assert.NoError(t, cmd.Validate())
|
||||
assert.NotZero(t, cmd.Timeout)
|
||||
|
||||
cmd = Command{Statement: "Ok", Timeout: 1 * time.Second}
|
||||
|
||||
assert.NoError(t, cmd.Validate())
|
||||
assert.Equal(t, 1*time.Second, cmd.Timeout)
|
||||
}
|
||||
|
||||
func TestCommandValidateArguments(t *testing.T) {
|
||||
cmd := Command{Statement: "Ok", Arguments: Arguments{
|
||||
Named: map[string]interface{}{"key": "val"},
|
||||
Positional: []interface{}{"val"},
|
||||
}}
|
||||
|
||||
assert.Error(t, cmd.Validate())
|
||||
}
|
||||
|
||||
func TestCommandUnmarshalJSON(t *testing.T) {
|
||||
strs := []string{
|
||||
"{\"statement\":\"Ok\"}",
|
||||
"{\"statement\":\"Ok\",\"arguments\":[0, 3.14, \"apple\"],\"mode\":\"query\"}",
|
||||
"{\"statement\":\"Ok\",\"isolation\":\"read_uncommitted\",\"timeout\":1000}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var cmd Command
|
||||
assert.NoError(t, json.Unmarshal([]byte(str), &cmd), str)
|
||||
}
|
||||
|
||||
strs = []string{
|
||||
"",
|
||||
"\"",
|
||||
"{}",
|
||||
"{\"argument\":{\"key\":\"val\"}}",
|
||||
"{\"statement\":[\"Ok\"]}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var cmd Command
|
||||
assert.Error(t, json.Unmarshal([]byte(str), &cmd), str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsValidateNotNil(t *testing.T) {
|
||||
args := Arguments{}
|
||||
|
||||
assert.NoError(t, args.Validate())
|
||||
assert.NotNil(t, args.Named)
|
||||
assert.NotNil(t, args.Positional)
|
||||
}
|
||||
|
||||
func TestArgumentsValidateMutuallyExclusive(t *testing.T) {
|
||||
args := []Arguments{
|
||||
Arguments{},
|
||||
Arguments{Named: map[string]interface{}{"key": "val"}},
|
||||
Arguments{Positional: []interface{}{"val"}},
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
assert.NoError(t, arg.Validate())
|
||||
assert.False(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
|
||||
}
|
||||
|
||||
args = []Arguments{
|
||||
Arguments{
|
||||
Named: map[string]interface{}{"key": "val"},
|
||||
Positional: []interface{}{"val"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
assert.Error(t, arg.Validate())
|
||||
assert.True(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsValidateKeys(t *testing.T) {
|
||||
keys := []string{
|
||||
"",
|
||||
"_",
|
||||
"_key",
|
||||
"1",
|
||||
"1key",
|
||||
"\xf0\x28\x8c\xbc", // non-utf8
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
args := Arguments{Named: map[string]interface{}{key: "val"}}
|
||||
|
||||
assert.Error(t, args.Validate(), key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsUnmarshalJSON(t *testing.T) {
|
||||
strs := []string{
|
||||
"{}",
|
||||
"{\"key\":\"val\"}",
|
||||
"{\"key\":[1, 3.14, {\"key\":\"val\"}]}",
|
||||
"[]",
|
||||
"[\"key\",\"val\"]",
|
||||
"[{}]",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var args Arguments
|
||||
assert.NoError(t, json.Unmarshal([]byte(str), &args), str)
|
||||
}
|
||||
|
||||
strs = []string{
|
||||
"",
|
||||
"\"",
|
||||
"1",
|
||||
"\"key\"",
|
||||
"{\"key\",\"val\"}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var args Arguments
|
||||
assert.Error(t, json.Unmarshal([]byte(str), &args), str)
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
)
|
||||
|
||||
// Cmd is the entrypoint command for dbconnect.
|
||||
//
|
||||
// The tunnel package is responsible for appending this to tunnel.Commands().
|
||||
func Cmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Category: "Database Connect (ALPHA)",
|
||||
Name: "db-connect",
|
||||
Usage: "Access your SQL database from Cloudflare Workers or the browser",
|
||||
ArgsUsage: " ",
|
||||
Description: `
|
||||
Creates a connection between your database and the Cloudflare edge.
|
||||
Now you can execute SQL commands anywhere you can send HTTPS requests.
|
||||
|
||||
Connect your database with any of the following commands, you can also try the "playground" without a database:
|
||||
|
||||
cloudflared db-connect --hostname sql.mysite.com --url postgres://user:pass@localhost?sslmode=disable \
|
||||
--auth-domain mysite.cloudflareaccess.com --application-aud my-access-policy-tag
|
||||
cloudflared db-connect --hostname sql-dev.mysite.com --url mysql://localhost --insecure
|
||||
cloudflared db-connect --playground
|
||||
|
||||
Requests should be authenticated using Cloudflare Access, learn more about how to enable it here:
|
||||
|
||||
https://developers.cloudflare.com/access/service-auth/service-token/
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "url",
|
||||
Usage: "URL to the database (eg. postgres://user:pass@localhost?sslmode=disable)",
|
||||
EnvVars: []string{"TUNNEL_URL"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "hostname",
|
||||
Usage: "Hostname to accept commands over HTTPS (eg. sql.mysite.com)",
|
||||
EnvVars: []string{"TUNNEL_HOSTNAME"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "auth-domain",
|
||||
Usage: "Cloudflare Access authentication domain for your account (eg. mysite.cloudflareaccess.com)",
|
||||
EnvVars: []string{"TUNNEL_ACCESS_AUTH_DOMAIN"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "application-aud",
|
||||
Usage: "Cloudflare Access application \"AUD\" to verify JWTs from requests",
|
||||
EnvVars: []string{"TUNNEL_ACCESS_APPLICATION_AUD"},
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "insecure",
|
||||
Usage: "Disable authentication, the database will be open to the Internet",
|
||||
Value: false,
|
||||
EnvVars: []string{"TUNNEL_ACCESS_INSECURE"},
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "playground",
|
||||
Usage: "Run a temporary, in-memory SQLite3 database for testing",
|
||||
Value: false,
|
||||
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "loglevel",
|
||||
Value: "debug", // Make it more verbose than the tunnel default 'info'.
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
Hidden: true,
|
||||
}),
|
||||
},
|
||||
Before: CmdBefore,
|
||||
Action: CmdAction,
|
||||
Hidden: true,
|
||||
}
|
||||
}
|
||||
|
||||
// CmdBefore runs some validation checks before running the command.
|
||||
func CmdBefore(c *cli.Context) error {
|
||||
// Show the help text is no flags are specified.
|
||||
if c.NumFlags() == 0 {
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
// Hello-world and playground are synonymous with each other,
|
||||
// unset hello-world to prevent tunnel from initializing the hello package.
|
||||
if c.IsSet("hello-world") {
|
||||
c.Set("playground", "true")
|
||||
c.Set("hello-world", "false")
|
||||
}
|
||||
|
||||
// Unix-socket database urls are supported, but the logic is the same as url.
|
||||
if c.IsSet("unix-socket") {
|
||||
c.Set("url", c.String("unix-socket"))
|
||||
c.Set("unix-socket", "")
|
||||
}
|
||||
|
||||
// When playground mode is enabled, run with an in-memory database.
|
||||
if c.IsSet("playground") {
|
||||
c.Set("url", "sqlite3::memory:?cache=shared")
|
||||
c.Set("insecure", strconv.FormatBool(!c.IsSet("auth-domain") && !c.IsSet("application-aud")))
|
||||
}
|
||||
|
||||
// At this point, insecure configurations are valid.
|
||||
if c.Bool("insecure") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure that secure configurations specify a hostname, domain, and tag for JWT validation.
|
||||
if !c.IsSet("hostname") || !c.IsSet("auth-domain") || !c.IsSet("application-aud") {
|
||||
log.Fatal("must specify --hostname, --auth-domain, and --application-aud unless you want to run in --insecure mode")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CmdAction starts the Proxy and sets the url in cli.Context to point to the Proxy address.
|
||||
func CmdAction(c *cli.Context) error {
|
||||
// STOR-612: sync with context in tunnel daemon.
|
||||
ctx := context.Background()
|
||||
|
||||
var proxy *Proxy
|
||||
var err error
|
||||
if c.Bool("insecure") {
|
||||
proxy, err = NewInsecureProxy(ctx, c.String("url"))
|
||||
} else {
|
||||
proxy, err = NewSecureProxy(ctx, c.String("url"), c.String("auth-domain"), c.String("application-aud"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
listenerC := make(chan net.Listener)
|
||||
defer close(listenerC)
|
||||
|
||||
// Since the Proxy should only talk to the tunnel daemon, find the next available
|
||||
// localhost port and start to listen to requests.
|
||||
go func() {
|
||||
err := proxy.Start(ctx, "127.0.0.1:", listenerC)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Block until the the Proxy is online, retreive its address, and change the url to point to it.
|
||||
// This is effectively "handing over" control to the tunnel package so it can run the tunnel daemon.
|
||||
c.Set("url", "https://"+(<-listenerC).Addr().String())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
tests := [][]string{
|
||||
{"cloudflared", "db-connect", "--playground"},
|
||||
{"cloudflared", "db-connect", "--playground", "--hostname", "sql.mysite.com"},
|
||||
{"cloudflared", "db-connect", "--url", "sqlite3::memory:?cache=shared", "--insecure"},
|
||||
{"cloudflared", "db-connect", "--url", "sqlite3::memory:?cache=shared", "--hostname", "sql.mysite.com", "--auth-domain", "mysite.cloudflareaccess.com", "--application-aud", "aud"},
|
||||
}
|
||||
|
||||
app := &cli.App{
|
||||
Name: "cloudflared",
|
||||
Commands: []*cli.Command{Cmd()},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.NoError(t, app.Run(test))
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
# docker-compose -f ./dbconnect/integration_test/dbconnect.yaml up --build --force-recreate --renew-anon-volumes --exit-code-from cloudflared
|
||||
|
||||
version: "2.3"
|
||||
networks:
|
||||
test-dbconnect-network:
|
||||
driver: bridge
|
||||
services:
|
||||
cloudflared:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: dev.Dockerfile
|
||||
command: go test github.com/cloudflare/cloudflared/dbconnect/integration_test -v
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
mssql:
|
||||
condition: service_healthy
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DBCONNECT_INTEGRATION_TEST: "true"
|
||||
POSTGRESQL_URL: postgres://postgres:secret@postgres/db?sslmode=disable
|
||||
MYSQL_URL: mysql://root:secret@mysql/db?tls=false
|
||||
MSSQL_URL: mssql://sa:secret12345!@mssql
|
||||
CLICKHOUSE_URL: clickhouse://clickhouse:9000/db
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
postgres:
|
||||
image: postgres:11.4-alpine
|
||||
environment:
|
||||
POSTGRES_DB: db
|
||||
POSTGRES_PASSWORD: secret
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "postgres"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_DATABASE: db
|
||||
MYSQL_ROOT_PASSWORD: secret
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2017-CU8-ubuntu
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
SA_PASSWORD: secret12345!
|
||||
healthcheck:
|
||||
test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "localhost", "-U", "sa", "-P", "secret12345!", "-Q", "SELECT 1"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
clickhouse:
|
||||
image: yandex/clickhouse-server:19.11
|
||||
healthcheck:
|
||||
test: ["CMD", "clickhouse-client", "--query", "SELECT 1"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
@@ -1,265 +0,0 @@
|
||||
package dbconnect_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/dbconnect"
|
||||
)
|
||||
|
||||
func TestIntegrationPostgreSQL(t *testing.T) {
|
||||
ctx, pq := helperNewSQLClient(t, "POSTGRESQL_URL")
|
||||
|
||||
err := pq.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a TEXT, b UUID, c JSON, d INET[], e SERIAL);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES ($1, $2, $3, $4);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"text",
|
||||
"6b8d686d-bd8e-43bc-b09a-cfcbbe702c10",
|
||||
"{\"bool\":true,\"array\":[\"a\", 1, 3.14],\"embed\":{\"num\":21}}",
|
||||
[]string{"1.1.1.1", "1.0.0.1"},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "UPDATE t SET b = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "text",
|
||||
"b": nil,
|
||||
"c": map[string]interface{}{
|
||||
"bool": true,
|
||||
"array": []interface{}{"a", float64(1), 3.14},
|
||||
"embed": map[string]interface{}{"num": float64(21)},
|
||||
},
|
||||
"d": "{1.1.1.1,1.0.0.1}",
|
||||
"e": int64(1),
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationMySQL(t *testing.T) {
|
||||
ctx, my := helperNewSQLClient(t, "MYSQL_URL")
|
||||
|
||||
err := my.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a CHAR, b TINYINT, c FLOAT, d JSON, e YEAR);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"a",
|
||||
10,
|
||||
3.14,
|
||||
"{\"bool\":true}",
|
||||
2000,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "ALTER TABLE t ADD COLUMN f GEOMETRY;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "a",
|
||||
"b": float64(10),
|
||||
"c": 3.14,
|
||||
"d": map[string]interface{}{"bool": true},
|
||||
"e": float64(2000),
|
||||
"f": nil,
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationMSSQL(t *testing.T) {
|
||||
ctx, ms := helperNewSQLClient(t, "MSSQL_URL")
|
||||
|
||||
err := ms.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a BIT, b DECIMAL, c MONEY, d TEXT);",
|
||||
Mode: "exec"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
0,
|
||||
3,
|
||||
"$0.99",
|
||||
"text",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "UPDATE t SET d = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": false,
|
||||
"b": float64(3),
|
||||
"c": float64(0.99),
|
||||
"d": nil,
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationClickhouse(t *testing.T) {
|
||||
ctx, ch := helperNewSQLClient(t, "CLICKHOUSE_URL")
|
||||
|
||||
err := ch.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a UUID, b String, c Float64, d UInt32, e Int16, f Array(Enum8('a'=1, 'b'=2, 'c'=3))) engine=Memory;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"ec65f626-6f50-4c86-9628-6314ef1edacd",
|
||||
"",
|
||||
3.14,
|
||||
314,
|
||||
-144,
|
||||
[]string{"a", "b", "c"},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "ec65f626-6f50-4c86-9628-6314ef1edacd",
|
||||
"b": "",
|
||||
"c": float64(3.14),
|
||||
"d": uint32(314),
|
||||
"e": int16(-144),
|
||||
"f": []string{"a", "b", "c"},
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func helperNewSQLClient(t *testing.T, env string) (context.Context, dbconnect.Client) {
|
||||
_, ok := os.LookupEnv("DBCONNECT_INTEGRATION_TEST")
|
||||
if ok {
|
||||
t.Helper()
|
||||
} else {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
val, ok := os.LookupEnv(env)
|
||||
if !ok {
|
||||
log.Fatalf("must provide database url as environment variable: %s", env)
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(val)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot provide invalid database url: %s=%s", env, val)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := dbconnect.NewSQLClient(ctx, parsed)
|
||||
if err != nil {
|
||||
log.Fatalf("could not start test client: %s", err)
|
||||
}
|
||||
|
||||
return ctx, client
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
timing "github.com/mitchellh/go-server-timing"
|
||||
)
|
||||
|
||||
// Proxy is an HTTP server that proxies requests to a Client.
|
||||
type Proxy struct {
|
||||
client Client
|
||||
accessValidator *validation.Access
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
// NewInsecureProxy creates a Proxy that talks to a Client at an origin.
|
||||
//
|
||||
// In insecure mode, the Proxy will allow all Command requests.
|
||||
func NewInsecureProxy(ctx context.Context, origin string) (*Proxy, error) {
|
||||
originURL, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "must provide a valid database url")
|
||||
}
|
||||
|
||||
client, err := NewClient(ctx, originURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = client.Ping(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not connect to the database")
|
||||
}
|
||||
|
||||
return &Proxy{client, nil, logrus.New()}, nil
|
||||
}
|
||||
|
||||
// NewSecureProxy creates a Proxy that talks to a Client at an origin.
|
||||
//
|
||||
// In secure mode, the Proxy will reject any Command requests that are
|
||||
// not authenticated by Cloudflare Access with a valid JWT.
|
||||
func NewSecureProxy(ctx context.Context, origin, authDomain, applicationAUD string) (*Proxy, error) {
|
||||
proxy, err := NewInsecureProxy(ctx, origin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validator, err := validation.NewAccessValidator(ctx, authDomain, authDomain, applicationAUD)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy.accessValidator = validator
|
||||
return proxy, err
|
||||
}
|
||||
|
||||
// IsInsecure gets whether the Proxy will accept a Command from any source.
|
||||
func (proxy *Proxy) IsInsecure() bool {
|
||||
return proxy.accessValidator == nil
|
||||
}
|
||||
|
||||
// IsAllowed checks whether a http.Request is allowed to receive data.
|
||||
//
|
||||
// By default, requests must pass through Cloudflare Access for authentication.
|
||||
// If the proxy is explcitly set to insecure mode, all requests will be allowed.
|
||||
func (proxy *Proxy) IsAllowed(r *http.Request, verbose ...bool) bool {
|
||||
if proxy.IsInsecure() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Access and Tunnel should prevent bad JWTs from even reaching the origin,
|
||||
// but validate tokens anyway as an abundance of caution.
|
||||
err := proxy.accessValidator.ValidateRequest(r.Context(), r)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Warn administrators that invalid JWTs are being rejected. This is indicative
|
||||
// of either a misconfiguration of the CLI or a massive failure of upstream systems.
|
||||
if len(verbose) > 0 {
|
||||
proxy.httpLog(r, err).Error("Failed JWT authentication")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Start the Proxy at a given address and notify the listener channel when the server is online.
|
||||
func (proxy *Proxy) Start(ctx context.Context, addr string, listenerC chan<- net.Listener) error {
|
||||
// STOR-611: use a seperate listener and consider web socket support.
|
||||
httpListener, err := hello.CreateTLSListener(addr)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not create listener at %s", addr)
|
||||
}
|
||||
|
||||
errC := make(chan error)
|
||||
defer close(errC)
|
||||
|
||||
// Starts the HTTP server and begins to serve requests.
|
||||
go func() {
|
||||
errC <- proxy.httpListen(ctx, httpListener)
|
||||
}()
|
||||
|
||||
// Continually ping the server until it comes online or 10 attempts fail.
|
||||
go func() {
|
||||
var err error
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err = http.Get("http://" + httpListener.Addr().String())
|
||||
|
||||
// Once no error was detected, notify the listener channel and return.
|
||||
if err == nil {
|
||||
listenerC <- httpListener
|
||||
return
|
||||
}
|
||||
|
||||
// Backoff between requests to ping the server.
|
||||
<-time.After(1 * time.Second)
|
||||
}
|
||||
errC <- errors.Wrap(err, "took too long for the http server to start")
|
||||
}()
|
||||
|
||||
return <-errC
|
||||
}
|
||||
|
||||
// httpListen starts the httpServer and blocks until the context closes.
|
||||
func (proxy *Proxy) httpListen(ctx context.Context, listener net.Listener) error {
|
||||
httpServer := &http.Server{
|
||||
Addr: listener.Addr().String(),
|
||||
Handler: timing.Middleware(proxy.httpRouter(), nil),
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
httpServer.Close()
|
||||
listener.Close()
|
||||
}()
|
||||
|
||||
return httpServer.Serve(listener)
|
||||
}
|
||||
|
||||
// httpRouter creates a mux.Router for the Proxy.
|
||||
func (proxy *Proxy) httpRouter() *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/ping", proxy.httpPing()).Methods("GET", "HEAD")
|
||||
router.HandleFunc("/submit", proxy.httpSubmit()).Methods("POST")
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// httpPing tests the connection to the database.
|
||||
//
|
||||
// By default, this endpoint is unauthenticated to allow for health checks.
|
||||
// To enable authentication, Cloudflare Access must be enabled on this route.
|
||||
func (proxy *Proxy) httpPing() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
metric := timing.FromContext(ctx).NewMetric("db").Start()
|
||||
err := proxy.client.Ping(ctx)
|
||||
metric.Stop()
|
||||
|
||||
if err == nil {
|
||||
proxy.httpRespond(w, r, http.StatusOK, "")
|
||||
} else {
|
||||
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// httpSubmit sends a command to the database and returns its response.
|
||||
//
|
||||
// By default, this endpoint will reject requests that do not pass through Cloudflare Access.
|
||||
// To disable authentication, the --insecure flag must be specified in the command line.
|
||||
func (proxy *Proxy) httpSubmit() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !proxy.IsAllowed(r, true) {
|
||||
proxy.httpRespondErr(w, r, http.StatusForbidden, fmt.Errorf(""))
|
||||
return
|
||||
}
|
||||
|
||||
var cmd Command
|
||||
err := json.NewDecoder(r.Body).Decode(&cmd)
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
metric := timing.FromContext(ctx).NewMetric("db").Start()
|
||||
data, err := proxy.client.Submit(ctx, &cmd)
|
||||
metric.Stop()
|
||||
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// httpRespond writes a status code and string response to the response writer.
|
||||
func (proxy *Proxy) httpRespond(w http.ResponseWriter, r *http.Request, status int, message string) {
|
||||
w.WriteHeader(status)
|
||||
|
||||
// Only expose the message detail of the reponse if the request is not HEAD
|
||||
// and the user is authenticated. For example, this prevents an unauthenticated
|
||||
// failed health check from accidentally leaking sensitive information about the Client.
|
||||
if r.Method != http.MethodHead && proxy.IsAllowed(r) {
|
||||
if message == "" {
|
||||
message = http.StatusText(status)
|
||||
}
|
||||
fmt.Fprint(w, message)
|
||||
}
|
||||
}
|
||||
|
||||
// httpRespondErr is similar to httpRespond, except it formats errors to be more friendly.
|
||||
func (proxy *Proxy) httpRespondErr(w http.ResponseWriter, r *http.Request, defaultStatus int, err error) {
|
||||
status, err := httpError(defaultStatus, err)
|
||||
|
||||
proxy.httpRespond(w, r, status, err.Error())
|
||||
proxy.httpLog(r, err).Warn("Database connect error")
|
||||
}
|
||||
|
||||
// httpLog returns a logrus.Entry that is formatted to output a request Cf-ray.
|
||||
func (proxy *Proxy) httpLog(r *http.Request, err error) *logrus.Entry {
|
||||
return proxy.logger.WithContext(r.Context()).WithField("CF-RAY", r.Header.Get("Cf-ray")).WithError(err)
|
||||
}
|
||||
|
||||
// httpError extracts common errors and returns an status code and friendly error.
|
||||
func httpError(defaultStatus int, err error) (int, error) {
|
||||
if err == nil {
|
||||
return http.StatusNotImplemented, fmt.Errorf("error expected but found none")
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return http.StatusBadRequest, fmt.Errorf("request body cannot be empty")
|
||||
}
|
||||
|
||||
if err == context.DeadlineExceeded {
|
||||
return http.StatusRequestTimeout, err
|
||||
}
|
||||
|
||||
_, ok := err.(net.Error)
|
||||
if ok {
|
||||
return http.StatusRequestTimeout, err
|
||||
}
|
||||
|
||||
if err == context.Canceled {
|
||||
// Does not exist in Golang, but would be: http.StatusClientClosedWithoutResponse
|
||||
return 444, err
|
||||
}
|
||||
|
||||
return defaultStatus, err
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewInsecureProxy(t *testing.T) {
|
||||
origins := []string{
|
||||
"",
|
||||
":/",
|
||||
"http://localhost",
|
||||
"tcp://localhost:9000?debug=true",
|
||||
"mongodb://127.0.0.1",
|
||||
}
|
||||
|
||||
for _, origin := range origins {
|
||||
proxy, err := NewInsecureProxy(context.Background(), origin)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, proxy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyIsAllowed(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
req := httptest.NewRequest("GET", "https://1.1.1.1/ping", nil)
|
||||
assert.True(t, proxy.IsAllowed(req))
|
||||
|
||||
proxy = helperNewProxy(t, true)
|
||||
req.Header.Set("Cf-access-jwt-assertion", "xxx")
|
||||
assert.False(t, proxy.IsAllowed(req))
|
||||
}
|
||||
|
||||
func TestProxyStart(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
ctx := context.Background()
|
||||
listenerC := make(chan net.Listener)
|
||||
|
||||
err := proxy.Start(ctx, "1.1.1.1:", listenerC)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = proxy.Start(ctx, "127.0.0.1:-1", listenerC)
|
||||
assert.Error(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 0)
|
||||
defer cancel()
|
||||
|
||||
err = proxy.Start(ctx, "127.0.0.1:", listenerC)
|
||||
assert.IsType(t, http.ErrServerClosed, err)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRouter(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
router := proxy.httpRouter()
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
method string
|
||||
valid bool
|
||||
}{
|
||||
{"", "GET", false},
|
||||
{"/", "GET", false},
|
||||
{"/ping", "GET", true},
|
||||
{"/ping", "HEAD", true},
|
||||
{"/ping", "POST", false},
|
||||
{"/submit", "POST", true},
|
||||
{"/submit", "GET", false},
|
||||
{"/submit/extra", "POST", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
match := &mux.RouteMatch{}
|
||||
ok := router.Match(httptest.NewRequest(test.method, "https://1.1.1.1"+test.path, nil), match)
|
||||
|
||||
assert.True(t, ok == test.valid, test.path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPPing(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(proxy.httpPing())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Equal(t, int64(2), res.ContentLength)
|
||||
|
||||
res, err = client.Head(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Equal(t, int64(-1), res.ContentLength)
|
||||
}
|
||||
|
||||
func TestProxyHTTPSubmit(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(proxy.httpSubmit())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
status int
|
||||
output string
|
||||
}{
|
||||
{"", http.StatusBadRequest, "request body cannot be empty"},
|
||||
{"{}", http.StatusBadRequest, "cannot provide an empty statement"},
|
||||
{"{\"statement\":\"Ok\"}", http.StatusUnprocessableEntity, "cannot provide invalid sql mode: ''"},
|
||||
{"{\"statement\":\"Ok\",\"mode\":\"query\"}", http.StatusUnprocessableEntity, "near \"Ok\": syntax error"},
|
||||
{"{\"statement\":\"CREATE TABLE t (a INT);\",\"mode\":\"exec\"}", http.StatusOK, "{\"last_insert_id\":0,\"rows_affected\":0}\n"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res, err := client.Post(server.URL, "application/json", strings.NewReader(test.input))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.status, res.StatusCode)
|
||||
if res.StatusCode > http.StatusOK {
|
||||
assert.Equal(t, "text/plain; charset=utf-8", res.Header.Get("Content-type"))
|
||||
} else {
|
||||
assert.Equal(t, "application/json", res.Header.Get("Content-type"))
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
str := string(data)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.output, str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPSubmitForbidden(t *testing.T) {
|
||||
proxy := helperNewProxy(t, true)
|
||||
|
||||
server := httptest.NewServer(proxy.httpSubmit())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
||||
assert.Zero(t, res.ContentLength)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRespond(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
|
||||
}))
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.Equal(t, int64(5), res.ContentLength)
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
assert.Equal(t, []byte("Hello"), data)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRespondForbidden(t *testing.T) {
|
||||
proxy := helperNewProxy(t, true)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
|
||||
}))
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.Equal(t, int64(0), res.ContentLength)
|
||||
}
|
||||
|
||||
func TestHTTPError(t *testing.T) {
|
||||
_, errTimeout := net.DialTimeout("tcp", "127.0.0.1", 0)
|
||||
assert.Error(t, errTimeout)
|
||||
|
||||
tests := []struct {
|
||||
input error
|
||||
status int
|
||||
output error
|
||||
}{
|
||||
{nil, http.StatusNotImplemented, fmt.Errorf("error expected but found none")},
|
||||
{io.EOF, http.StatusBadRequest, fmt.Errorf("request body cannot be empty")},
|
||||
{context.DeadlineExceeded, http.StatusRequestTimeout, nil},
|
||||
{context.Canceled, 444, nil},
|
||||
{errTimeout, http.StatusRequestTimeout, nil},
|
||||
{fmt.Errorf(""), http.StatusInternalServerError, nil},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
status, err := httpError(http.StatusInternalServerError, test.input)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, test.status, status)
|
||||
if test.output == nil {
|
||||
test.output = test.input
|
||||
}
|
||||
assert.Equal(t, test.output, err)
|
||||
}
|
||||
}
|
||||
|
||||
func helperNewProxy(t *testing.T, secure ...bool) *Proxy {
|
||||
t.Helper()
|
||||
|
||||
proxy, err := NewSecureProxy(context.Background(), "file::memory:?cache=shared", "test.cloudflareaccess.com", "")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, proxy)
|
||||
|
||||
if len(secure) == 0 {
|
||||
proxy.accessValidator = nil // Mark as insecure
|
||||
}
|
||||
|
||||
return proxy
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/xo/dburl"
|
||||
|
||||
// SQL drivers self-register with the database/sql package.
|
||||
// https://github.com/golang/go/wiki/SQLDrivers
|
||||
_ "github.com/denisenkom/go-mssqldb"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/kshvakov/clickhouse"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// SQLClient is a Client that talks to a SQL database.
|
||||
type SQLClient struct {
|
||||
Dialect string
|
||||
driver *sqlx.DB
|
||||
}
|
||||
|
||||
// NewSQLClient creates a SQL client based on its URL scheme.
|
||||
func NewSQLClient(ctx context.Context, originURL *url.URL) (Client, error) {
|
||||
res, err := dburl.Parse(originURL.String())
|
||||
if err != nil {
|
||||
helpText := fmt.Sprintf("supported drivers: %+q, see documentation for more details: %s", sql.Drivers(), "https://godoc.org/github.com/xo/dburl")
|
||||
return nil, fmt.Errorf("could not parse sql database url '%s': %s\n%s", originURL, err.Error(), helpText)
|
||||
}
|
||||
|
||||
// Establishes the driver, but does not test the connection.
|
||||
driver, err := sqlx.Open(res.Driver, res.DSN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open sql driver %s: %s\n%s", res.Driver, err.Error(), res.DSN)
|
||||
}
|
||||
|
||||
// Closes the driver, will occur when the context finishes.
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
driver.Close()
|
||||
}()
|
||||
|
||||
return &SQLClient{driver.DriverName(), driver}, nil
|
||||
}
|
||||
|
||||
// Ping verifies a connection to the database is still alive.
|
||||
func (client *SQLClient) Ping(ctx context.Context) error {
|
||||
return client.driver.PingContext(ctx)
|
||||
}
|
||||
|
||||
// Submit queries or executes a command to the SQL database.
|
||||
func (client *SQLClient) Submit(ctx context.Context, cmd *Command) (interface{}, error) {
|
||||
txx, err := cmd.ValidateSQL(client.Dialect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, cmd.Timeout)
|
||||
defer cancel()
|
||||
|
||||
var res interface{}
|
||||
|
||||
// Get the next available sql.Conn and submit the Command.
|
||||
err = sqlConn(ctx, client.driver, txx, func(conn *sql.Conn) error {
|
||||
stmt := cmd.Statement
|
||||
args := cmd.Arguments.Positional
|
||||
|
||||
if cmd.Mode == "query" {
|
||||
res, err = sqlQuery(ctx, conn, stmt, args)
|
||||
} else {
|
||||
res, err = sqlExec(ctx, conn, stmt, args)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ValidateSQL extends the contract of Command for SQL dialects:
|
||||
// mode is conformed, arguments are []sql.NamedArg, and isolation is a sql.IsolationLevel.
|
||||
//
|
||||
// When the command should not be wrapped in a transaction, *sql.TxOptions and error will both be nil.
|
||||
func (cmd *Command) ValidateSQL(dialect string) (*sql.TxOptions, error) {
|
||||
err := cmd.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mode, err := sqlMode(cmd.Mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mutates Arguments to only use positional arguments with the type sql.NamedArg.
|
||||
// This is a required by the sql.Driver before submitting arguments.
|
||||
cmd.Arguments.sql(dialect)
|
||||
|
||||
iso, err := sqlIsolation(cmd.Isolation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// When isolation is out-of-range, this is indicative that no
|
||||
// transaction should be executed and sql.TxOptions should be nil.
|
||||
if iso < sql.LevelDefault {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// In query mode, execute the transaction in read-only, unless it's Microsoft SQL
|
||||
// which does not support that type of transaction.
|
||||
readOnly := mode == "query" && dialect != "mssql"
|
||||
|
||||
return &sql.TxOptions{Isolation: iso, ReadOnly: readOnly}, nil
|
||||
}
|
||||
|
||||
// sqlConn gets the next available sql.Conn in the connection pool and runs a function to use it.
|
||||
//
|
||||
// If the transaction options are nil, run the useIt function outside a transaction.
|
||||
// This is potentially an unsafe operation if the command does not clean up its state.
|
||||
func sqlConn(ctx context.Context, driver *sqlx.DB, txx *sql.TxOptions, useIt func(*sql.Conn) error) error {
|
||||
conn, err := driver.Conn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// If transaction options are specified, begin and defer a rollback to catch errors.
|
||||
var tx *sql.Tx
|
||||
if txx != nil {
|
||||
tx, err = conn.BeginTx(ctx, txx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
}
|
||||
|
||||
err = useIt(conn)
|
||||
|
||||
// Check if useIt was successful and a transaction exists before committing.
|
||||
if err == nil && tx != nil {
|
||||
err = tx.Commit()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// sqlQuery queries rows on a sql.Conn and returns an array of result objects.
|
||||
func sqlQuery(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) ([]map[string]interface{}, error) {
|
||||
rows, err := conn.QueryContext(ctx, stmt, args...)
|
||||
if err == nil {
|
||||
return sqlRows(rows)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// sqlExec executes a command on a sql.Conn and returns the result of the operation.
|
||||
func sqlExec(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) (sqlResult, error) {
|
||||
exec, err := conn.ExecContext(ctx, stmt, args...)
|
||||
if err == nil {
|
||||
return sqlResultFrom(exec), nil
|
||||
}
|
||||
return sqlResult{}, err
|
||||
}
|
||||
|
||||
// sql mutates Arguments to contain a positional []sql.NamedArg.
|
||||
//
|
||||
// The actual return type is []interface{} due to the native Golang
|
||||
// function signatures for sql.Exec and sql.Query being generic.
|
||||
func (args *Arguments) sql(dialect string) {
|
||||
result := args.Positional
|
||||
|
||||
for i, val := range result {
|
||||
result[i] = sqlArg("", val, dialect)
|
||||
}
|
||||
|
||||
for key, val := range args.Named {
|
||||
result = append(result, sqlArg(key, val, dialect))
|
||||
}
|
||||
|
||||
args.Positional = result
|
||||
args.Named = map[string]interface{}{}
|
||||
}
|
||||
|
||||
// sqlArg creates a sql.NamedArg from a key-value pair and an optional dialect.
|
||||
//
|
||||
// Certain dialects will need to wrap objects, such as arrays, to conform its driver requirements.
|
||||
func sqlArg(key, val interface{}, dialect string) sql.NamedArg {
|
||||
switch reflect.ValueOf(val).Kind() {
|
||||
|
||||
// PostgreSQL and Clickhouse require arrays to be wrapped before
|
||||
// being inserted into the driver interface.
|
||||
case reflect.Slice, reflect.Array:
|
||||
switch dialect {
|
||||
case "postgres":
|
||||
val = pq.Array(val)
|
||||
case "clickhouse":
|
||||
val = clickhouse.Array(val)
|
||||
}
|
||||
}
|
||||
|
||||
return sql.Named(fmt.Sprint(key), val)
|
||||
}
|
||||
|
||||
// sqlIsolation tries to match a string to a sql.IsolationLevel.
|
||||
func sqlIsolation(str string) (sql.IsolationLevel, error) {
|
||||
if str == "none" {
|
||||
return sql.IsolationLevel(-1), nil
|
||||
}
|
||||
|
||||
for iso := sql.LevelDefault; ; iso++ {
|
||||
if iso > sql.LevelLinearizable {
|
||||
return -1, fmt.Errorf("cannot provide an invalid sql isolation level: '%s'", str)
|
||||
}
|
||||
|
||||
if str == "" || strings.EqualFold(iso.String(), strings.ReplaceAll(str, "_", " ")) {
|
||||
return iso, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sqlMode tries to match a string to a command mode: 'query' or 'exec' for now.
|
||||
func sqlMode(str string) (string, error) {
|
||||
switch str {
|
||||
case "query", "exec":
|
||||
return str, nil
|
||||
default:
|
||||
return "", fmt.Errorf("cannot provide invalid sql mode: '%s'", str)
|
||||
}
|
||||
}
|
||||
|
||||
// sqlRows scans through a SQL result set and returns an array of objects.
|
||||
func sqlRows(rows *sql.Rows) ([]map[string]interface{}, error) {
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not extract columns from result")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
types, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
// Some drivers do not support type extraction, so fail silently and continue.
|
||||
types = make([]*sql.ColumnType, len(columns))
|
||||
}
|
||||
|
||||
values := make([]interface{}, len(columns))
|
||||
pointers := make([]interface{}, len(columns))
|
||||
|
||||
var results []map[string]interface{}
|
||||
for rows.Next() {
|
||||
for i := range columns {
|
||||
pointers[i] = &values[i]
|
||||
}
|
||||
rows.Scan(pointers...)
|
||||
|
||||
// Convert a row, an array of values, into an object where
|
||||
// each key is the name of its respective column.
|
||||
entry := make(map[string]interface{})
|
||||
for i, col := range columns {
|
||||
entry[col] = sqlValue(values[i], types[i])
|
||||
}
|
||||
results = append(results, entry)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// sqlValue handles special cases where sql.Rows does not return a "human-readable" object.
|
||||
func sqlValue(val interface{}, col *sql.ColumnType) interface{} {
|
||||
bytes, ok := val.([]byte)
|
||||
if ok {
|
||||
// Opportunistically check for embeded JSON and convert it to a first-class object.
|
||||
var embeded interface{}
|
||||
if json.Unmarshal(bytes, &embeded) == nil {
|
||||
return embeded
|
||||
}
|
||||
|
||||
// STOR-604: investigate a way to coerce PostgreSQL arrays '{a, b, ...}' into JSON.
|
||||
// Although easy with strings, it becomes more difficult with special types like INET[].
|
||||
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// sqlResult is a thin wrapper around sql.Result.
|
||||
type sqlResult struct {
|
||||
LastInsertId int64 `json:"last_insert_id"`
|
||||
RowsAffected int64 `json:"rows_affected"`
|
||||
}
|
||||
|
||||
// sqlResultFrom converts sql.Result into a JSON-marshable sqlResult.
|
||||
func sqlResultFrom(res sql.Result) sqlResult {
|
||||
insertID, errID := res.LastInsertId()
|
||||
rowsAffected, errRows := res.RowsAffected()
|
||||
|
||||
// If an error occurs when extracting the result, it is because the
|
||||
// driver does not support that specific field. Instead of passing this
|
||||
// to the user, omit the field in the response.
|
||||
if errID != nil {
|
||||
insertID = -1
|
||||
}
|
||||
if errRows != nil {
|
||||
rowsAffected = -1
|
||||
}
|
||||
|
||||
return sqlResult{insertID, rowsAffected}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kshvakov/clickhouse"
|
||||
"github.com/lib/pq"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewSQLClient(t *testing.T) {
|
||||
originURLs := []string{
|
||||
"postgres://localhost",
|
||||
"cockroachdb://localhost:1337",
|
||||
"postgresql://user:pass@127.0.0.1",
|
||||
"mysql://localhost",
|
||||
"clickhouse://127.0.0.1:9000/?debug",
|
||||
"sqlite3::memory:",
|
||||
"file:test.db?cache=shared",
|
||||
}
|
||||
|
||||
for _, originURL := range originURLs {
|
||||
origin, _ := url.Parse(originURL)
|
||||
_, err := NewSQLClient(context.Background(), origin)
|
||||
|
||||
assert.NoError(t, err, originURL)
|
||||
}
|
||||
|
||||
originURLs = []string{
|
||||
"",
|
||||
"/",
|
||||
"http://localhost",
|
||||
"coolthing://user:pass@127.0.0.1",
|
||||
}
|
||||
|
||||
for _, originURL := range originURLs {
|
||||
origin, _ := url.Parse(originURL)
|
||||
_, err := NewSQLClient(context.Background(), origin)
|
||||
|
||||
assert.Error(t, err, originURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsSQL(t *testing.T) {
|
||||
args := []Arguments{
|
||||
Arguments{
|
||||
Positional: []interface{}{
|
||||
"val", 10, 3.14,
|
||||
},
|
||||
},
|
||||
Arguments{
|
||||
Named: map[string]interface{}{
|
||||
"key": time.Unix(0, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var nameType sql.NamedArg
|
||||
for _, arg := range args {
|
||||
arg.sql("")
|
||||
for _, named := range arg.Positional {
|
||||
assert.IsType(t, nameType, named)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLArg(t *testing.T) {
|
||||
tests := []struct {
|
||||
key interface{}
|
||||
val interface{}
|
||||
dialect string
|
||||
arg sql.NamedArg
|
||||
}{
|
||||
{"key", "val", "mssql", sql.Named("key", "val")},
|
||||
{0, 1, "sqlite3", sql.Named("0", 1)},
|
||||
{1, []string{"a", "b", "c"}, "postgres", sql.Named("1", pq.Array([]string{"a", "b", "c"}))},
|
||||
{"in", []uint{0, 1}, "clickhouse", sql.Named("in", clickhouse.Array([]uint{0, 1}))},
|
||||
{"", time.Unix(0, 0), "", sql.Named("", time.Unix(0, 0))},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
arg := sqlArg(test.key, test.val, test.dialect)
|
||||
assert.Equal(t, test.arg, arg, test.key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLIsolation(t *testing.T) {
|
||||
tests := []struct {
|
||||
str string
|
||||
iso sql.IsolationLevel
|
||||
}{
|
||||
{"", sql.LevelDefault},
|
||||
{"DEFAULT", sql.LevelDefault},
|
||||
{"read_UNcommitted", sql.LevelReadUncommitted},
|
||||
{"serializable", sql.LevelSerializable},
|
||||
{"none", sql.IsolationLevel(-1)},
|
||||
{"SNAP shot", -2},
|
||||
{"blah", -2},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
iso, err := sqlIsolation(test.str)
|
||||
|
||||
if test.iso < -1 {
|
||||
assert.Error(t, err, test.str)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.iso, iso, test.str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLMode(t *testing.T) {
|
||||
modes := []string{
|
||||
"query",
|
||||
"exec",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
actual, err := sqlMode(mode)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.ToLower(mode), actual, mode)
|
||||
}
|
||||
|
||||
modes = []string{
|
||||
"",
|
||||
"blah",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
_, err := sqlMode(mode)
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func helperRows(mockRows *sqlmock.Rows) *sql.Rows {
|
||||
db, mock, _ := sqlmock.New()
|
||||
mock.ExpectQuery("SELECT").WillReturnRows(mockRows)
|
||||
rows, _ := db.Query("SELECT")
|
||||
return rows
|
||||
}
|
||||
|
||||
func TestSQLRows(t *testing.T) {
|
||||
actual, err := sqlRows(helperRows(sqlmock.NewRows(
|
||||
[]string{"name", "age", "dept"}).
|
||||
AddRow("alice", 19, "prod")))
|
||||
expected := []map[string]interface{}{map[string]interface{}{
|
||||
"name": "alice",
|
||||
"age": int64(19),
|
||||
"dept": "prod"}}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestSQLValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
input interface{}
|
||||
output interface{}
|
||||
}{
|
||||
{"hello", "hello"},
|
||||
{1, 1},
|
||||
{false, false},
|
||||
{[]byte("random"), "random"},
|
||||
{[]byte("{\"json\":true}"), map[string]interface{}{"json": true}},
|
||||
{[]byte("[]"), []interface{}{}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.output, sqlValue(test.input, nil), test.input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLResultFrom(t *testing.T) {
|
||||
res := sqlResultFrom(sqlmock.NewResult(1, 2))
|
||||
assert.Equal(t, sqlResult{1, 2}, res)
|
||||
|
||||
res = sqlResultFrom(sqlmock.NewErrorResult(fmt.Errorf("")))
|
||||
assert.Equal(t, sqlResult{-1, -1}, res)
|
||||
}
|
||||
|
||||
func helperSQLite3(t *testing.T) (context.Context, Client) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
url, _ := url.Parse("file::memory:?cache=shared")
|
||||
|
||||
sqlite3, err := NewSQLClient(ctx, url)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return ctx, sqlite3
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
err := sqlite3.Ping(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSubmit(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "CREATE TABLE t (a INTEGER, b FLOAT, c TEXT, d BLOB);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{0, 0}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: Arguments{
|
||||
Positional: []interface{}{
|
||||
1,
|
||||
3.14,
|
||||
"text",
|
||||
"blob",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "UPDATE t SET c = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t WHERE a = ?;",
|
||||
Mode: "query",
|
||||
Arguments: Arguments{
|
||||
Positional: []interface{}{1},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, 1)
|
||||
|
||||
resf, ok := res.([]map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": 3.14,
|
||||
"c": nil,
|
||||
"d": "blob",
|
||||
}, resf[0])
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
}
|
||||
|
||||
func TestSubmitTransaction(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "BEGIN;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "BEGIN; CREATE TABLE tt (a INT); COMMIT;",
|
||||
Mode: "exec",
|
||||
Isolation: "none",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{0, 0}, res)
|
||||
|
||||
rows, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM tt;",
|
||||
Mode: "query",
|
||||
Isolation: "repeatable_read",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, rows)
|
||||
}
|
||||
|
||||
func TestSubmitTimeout(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
Timeout: 1 * time.Nanosecond,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
|
||||
func TestSubmitMode(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "notanoption",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
|
||||
func TestSubmitEmpty(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "; ; ; ;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
FROM golang:1.12 as builder
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
RUN apt-get update
|
||||
COPY . .
|
||||
+6
-5
@@ -20,11 +20,12 @@ var (
|
||||
ErrUnknownStream = MuxerProtocolError{"2002 unknown stream", http2.ErrCodeProtocol}
|
||||
ErrInvalidStream = MuxerProtocolError{"2003 invalid stream", http2.ErrCodeProtocol}
|
||||
|
||||
ErrStreamHeadersSent = MuxerApplicationError{"3000 headers already sent"}
|
||||
ErrConnectionClosed = MuxerApplicationError{"3001 connection closed"}
|
||||
ErrConnectionDropped = MuxerApplicationError{"3002 connection dropped"}
|
||||
ErrOpenStreamTimeout = MuxerApplicationError{"3003 open stream timeout"}
|
||||
ErrResponseHeadersTimeout = MuxerApplicationError{"3004 timeout waiting for initial response headers"}
|
||||
ErrStreamHeadersSent = MuxerApplicationError{"3000 headers already sent"}
|
||||
ErrStreamRequestConnectionClosed = MuxerApplicationError{"3001 connection closed while opening stream"}
|
||||
ErrConnectionDropped = MuxerApplicationError{"3002 connection dropped"}
|
||||
ErrStreamRequestTimeout = MuxerApplicationError{"3003 open stream timeout"}
|
||||
ErrResponseHeadersTimeout = MuxerApplicationError{"3004 timeout waiting for initial response headers"}
|
||||
ErrResponseHeadersConnectionClosed = MuxerApplicationError{"3005 connection closed while waiting for initial response headers"}
|
||||
|
||||
ErrClosedStream = MuxerStreamError{"4000 stream closed", http2.ErrCodeStreamClosed}
|
||||
)
|
||||
|
||||
+35
-56
@@ -1,7 +1,6 @@
|
||||
package h2mux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
@@ -388,72 +387,52 @@ func isConnectionClosedError(err error) bool {
|
||||
// OpenStream opens a new data stream with the given headers.
|
||||
// Called by proxy server and tunnel
|
||||
func (m *Muxer) OpenStream(ctx context.Context, headers []Header, body io.Reader) (*MuxedStream, error) {
|
||||
stream := &MuxedStream{
|
||||
responseHeadersReceived: make(chan struct{}),
|
||||
readBuffer: NewSharedBuffer(),
|
||||
writeBuffer: &bytes.Buffer{},
|
||||
writeBufferMaxLen: m.config.StreamWriteBufferMaxLen,
|
||||
writeBufferHasSpace: make(chan struct{}, 1),
|
||||
receiveWindow: m.config.DefaultWindowSize,
|
||||
receiveWindowCurrentMax: m.config.DefaultWindowSize,
|
||||
receiveWindowMax: m.config.MaxWindowSize,
|
||||
sendWindow: m.config.DefaultWindowSize,
|
||||
readyList: m.readyList,
|
||||
writeHeaders: headers,
|
||||
dictionaries: m.muxReader.dictionaries,
|
||||
stream := m.NewStream(headers)
|
||||
if err := m.MakeMuxedStreamRequest(ctx, MuxedStreamRequest{stream, body}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.AwaitResponseHeaders(ctx, stream); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
|
||||
func (m *Muxer) OpenRPCStream(ctx context.Context) (*MuxedStream, error) {
|
||||
stream := m.NewStream(RPCHeaders())
|
||||
if err := m.MakeMuxedStreamRequest(ctx, MuxedStreamRequest{stream: stream, body: nil}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.AwaitResponseHeaders(ctx, stream); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func (m *Muxer) NewStream(headers []Header) *MuxedStream {
|
||||
return NewStream(m.config, headers, m.readyList, m.muxReader.dictionaries)
|
||||
}
|
||||
|
||||
func (m *Muxer) MakeMuxedStreamRequest(ctx context.Context, request MuxedStreamRequest) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ErrStreamRequestTimeout
|
||||
case <-m.abortChan:
|
||||
return ErrStreamRequestConnectionClosed
|
||||
// Will be received by mux writer
|
||||
case <-ctx.Done():
|
||||
return nil, ErrOpenStreamTimeout
|
||||
case <-m.abortChan:
|
||||
return nil, ErrConnectionClosed
|
||||
case m.newStreamChan <- MuxedStreamRequest{stream: stream, body: body}:
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ErrResponseHeadersTimeout
|
||||
case <-m.abortChan:
|
||||
return nil, ErrConnectionClosed
|
||||
case <-stream.responseHeadersReceived:
|
||||
return stream, nil
|
||||
case m.newStreamChan <- request:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Muxer) OpenRPCStream(ctx context.Context) (*MuxedStream, error) {
|
||||
stream := &MuxedStream{
|
||||
responseHeadersReceived: make(chan struct{}),
|
||||
readBuffer: NewSharedBuffer(),
|
||||
writeBuffer: &bytes.Buffer{},
|
||||
writeBufferMaxLen: m.config.StreamWriteBufferMaxLen,
|
||||
writeBufferHasSpace: make(chan struct{}, 1),
|
||||
receiveWindow: m.config.DefaultWindowSize,
|
||||
receiveWindowCurrentMax: m.config.DefaultWindowSize,
|
||||
receiveWindowMax: m.config.MaxWindowSize,
|
||||
sendWindow: m.config.DefaultWindowSize,
|
||||
readyList: m.readyList,
|
||||
writeHeaders: RPCHeaders(),
|
||||
dictionaries: m.muxReader.dictionaries,
|
||||
}
|
||||
|
||||
select {
|
||||
// Will be received by mux writer
|
||||
case <-ctx.Done():
|
||||
return nil, ErrOpenStreamTimeout
|
||||
case <-m.abortChan:
|
||||
return nil, ErrConnectionClosed
|
||||
case m.newStreamChan <- MuxedStreamRequest{stream: stream, body: nil}:
|
||||
}
|
||||
|
||||
func (m *Muxer) AwaitResponseHeaders(ctx context.Context, stream *MuxedStream) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ErrResponseHeadersTimeout
|
||||
return ErrResponseHeadersTimeout
|
||||
case <-m.abortChan:
|
||||
return nil, ErrConnectionClosed
|
||||
return ErrResponseHeadersConnectionClosed
|
||||
case <-stream.responseHeadersReceived:
|
||||
return stream, nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -584,8 +584,8 @@ func TestOpenAfterDisconnect(t *testing.T) {
|
||||
[]Header{{Name: "test-header", Value: "headerValue"}},
|
||||
nil,
|
||||
)
|
||||
if err != ErrConnectionClosed {
|
||||
t.Fatalf("unexpected error in OpenStream: %s", err)
|
||||
if err != ErrStreamRequestConnectionClosed && err != ErrResponseHeadersConnectionClosed {
|
||||
t.Fatalf("case %v: unexpected error in OpenStream: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,23 @@ func (th TunnelHostname) IsSet() bool {
|
||||
return th != ""
|
||||
}
|
||||
|
||||
func NewStream(config MuxerConfig, writeHeaders []Header, readyList *ReadyList, dictionaries h2Dictionaries) *MuxedStream {
|
||||
return &MuxedStream{
|
||||
responseHeadersReceived: make(chan struct{}),
|
||||
readBuffer: NewSharedBuffer(),
|
||||
writeBuffer: &bytes.Buffer{},
|
||||
writeBufferMaxLen: config.StreamWriteBufferMaxLen,
|
||||
writeBufferHasSpace: make(chan struct{}, 1),
|
||||
receiveWindow: config.DefaultWindowSize,
|
||||
receiveWindowCurrentMax: config.DefaultWindowSize,
|
||||
receiveWindowMax: config.MaxWindowSize,
|
||||
sendWindow: config.DefaultWindowSize,
|
||||
readyList: readyList,
|
||||
writeHeaders: writeHeaders,
|
||||
dictionaries: dictionaries,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MuxedStream) Read(p []byte) (n int, err error) {
|
||||
var readBuffer ReadWriteClosedCloser
|
||||
if s.dictionaries.read != nil {
|
||||
|
||||
+19
-6
@@ -53,6 +53,7 @@ type TunnelConfig struct {
|
||||
HTTPTransport http.RoundTripper
|
||||
HeartbeatInterval time.Duration
|
||||
Hostname string
|
||||
HTTPHostHeader string
|
||||
IncidentLookup IncidentLookup
|
||||
IsAutoupdated bool
|
||||
IsFreeTunnel bool
|
||||
@@ -447,12 +448,13 @@ func H1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
|
||||
}
|
||||
|
||||
type TunnelHandler struct {
|
||||
originUrl string
|
||||
muxer *h2mux.Muxer
|
||||
httpClient http.RoundTripper
|
||||
tlsConfig *tls.Config
|
||||
tags []tunnelpogs.Tag
|
||||
metrics *TunnelMetrics
|
||||
originUrl string
|
||||
httpHostHeader string
|
||||
muxer *h2mux.Muxer
|
||||
httpClient http.RoundTripper
|
||||
tlsConfig *tls.Config
|
||||
tags []tunnelpogs.Tag
|
||||
metrics *TunnelMetrics
|
||||
// connectionID is only used by metrics, and prometheus requires labels to be string
|
||||
connectionID string
|
||||
logger *log.Logger
|
||||
@@ -473,6 +475,7 @@ func NewTunnelHandler(ctx context.Context,
|
||||
}
|
||||
h := &TunnelHandler{
|
||||
originUrl: originURL,
|
||||
httpHostHeader: config.HTTPHostHeader,
|
||||
httpClient: config.HTTPTransport,
|
||||
tlsConfig: config.ClientTlsConfig,
|
||||
tags: config.Tags,
|
||||
@@ -566,6 +569,11 @@ func (h *TunnelHandler) createRequest(stream *h2mux.MuxedStream) (*http.Request,
|
||||
}
|
||||
|
||||
func (h *TunnelHandler) serveWebsocket(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
|
||||
if h.httpHostHeader != "" {
|
||||
req.Header.Set("Host", h.httpHostHeader)
|
||||
req.Host = h.httpHostHeader
|
||||
}
|
||||
|
||||
conn, response, err := websocket.ClientConnect(req, h.tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -594,6 +602,11 @@ func (h *TunnelHandler) serveHTTP(stream *h2mux.MuxedStream, req *http.Request)
|
||||
// Request origin to keep connection alive to improve performance
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
|
||||
if h.httpHostHeader != "" {
|
||||
req.Header.Set("Host", h.httpHostHeader)
|
||||
req.Host = h.httpHostHeader
|
||||
}
|
||||
|
||||
response, err := h.httpClient.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error proxying request to origin")
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM python:3-buster
|
||||
|
||||
RUN wget https://bin.equinox.io/c/VdrWdbjqyF/cloudflared-stable-linux-amd64.deb \
|
||||
&& dpkg -i cloudflared-stable-linux-amd64.deb
|
||||
|
||||
RUN pip install pexpect
|
||||
|
||||
COPY tests.py .
|
||||
COPY ssh /root/.ssh
|
||||
RUN chmod 600 /root/.ssh/id_rsa
|
||||
|
||||
ARG SSH_HOSTNAME
|
||||
RUN bash -c 'sed -i "s/{{hostname}}/${SSH_HOSTNAME}/g" /root/.ssh/authorized_keys_config'
|
||||
RUN bash -c 'sed -i "s/{{hostname}}/${SSH_HOSTNAME}/g" /root/.ssh/short_lived_cert_config'
|
||||
@@ -0,0 +1,23 @@
|
||||
# Cloudflared SSH server smoke tests
|
||||
|
||||
Runs several tests in a docker container against a server that is started out of band of these tests.
|
||||
Cloudflared token also needs to be retrieved out of band.
|
||||
SSH server hostname and user need to be configured in a docker environment file
|
||||
|
||||
|
||||
## Running tests
|
||||
|
||||
* Build cloudflared:
|
||||
make cloudflared
|
||||
|
||||
* Start server:
|
||||
sudo ./cloudflared tunnel --hostname HOSTNAME --ssh-server
|
||||
|
||||
* Fetch token:
|
||||
./cloudflared access login HOSTNAME
|
||||
|
||||
* Create docker env file:
|
||||
echo "SSH_HOSTNAME=HOSTNAME\nSSH_USER=USERNAME\n" > ssh_server_tests/.env
|
||||
|
||||
* Run tests:
|
||||
make test-ssh-server
|
||||
@@ -0,0 +1,19 @@
|
||||
version: "3.1"
|
||||
|
||||
services:
|
||||
ssh_test:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- SSH_HOSTNAME=${SSH_HOSTNAME}
|
||||
volumes:
|
||||
- "~/.cloudflared/:/root/.cloudflared"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- AUTHORIZED_KEYS_SSH_CONFIG=/root/.ssh/authorized_keys_config
|
||||
- SHORT_LIVED_CERT_SSH_CONFIG=/root/.ssh/short_lived_cert_config
|
||||
- REMOTE_SCP_FILENAME=scp_test.txt
|
||||
- ROOT_ONLY_TEST_FILE_PATH=~/permission_test.txt
|
||||
|
||||
entrypoint: "python tests.py"
|
||||
@@ -0,0 +1,5 @@
|
||||
Host *
|
||||
AddressFamily inet
|
||||
|
||||
Host {{hostname}}
|
||||
ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h
|
||||
@@ -0,0 +1,49 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAgEAvi26NDQ8cYTTztqPe9ZgF5HR/rIo5FoDgL5NbbZKW6h0txP9Fd8s
|
||||
id9Bgmo+aGkeM327tPVVMQ6UFmdRksOCIDWQDjNLF8b6S+Fu95tvMKSbGreRoR32OvgZKV
|
||||
I6KmOsF4z4GIv9naPplZswtKEUhSSI+/gPdAs9wfwalqZ77e82QJ727bYMeC3lzuoT+KBI
|
||||
dYufJ4OQhLtpHrqhB5sn7s6+oCv/u85GSln5SIC18Hi2t9lW4tgb5tH8P0kEDDWGfPS5ok
|
||||
qGi4kFTvwBXOCS2r4dhi5hRkpP7PqG4np0OCfvK5IRRJ27fCnj0loc+puZJAxnPMbuJr64
|
||||
vwxRx78PM/V0PDUsl0P6aR/vbe0XmF9FGqbWf2Tar1p4r6C9/bMzcDz8seYT8hzLIHP3+R
|
||||
l1hdlsTLm+1EzhaExKId+tjXegKGG4nU24h6qHEnRxLQDMwEsdkfj4E1pVypZJXVyNj99D
|
||||
o84vi0EUnu7R4HmQb/C+Pu7qMDtLT3Zk7O5Mg4LQ+cTz9V0noYEAyG46nAB4U/nJzBnV1J
|
||||
+aAdpioHmUAYhLYlQ9Kiy7LCJi92g9Wqa4wxMKxBbO5ZeH++p2p2lUi/oQNqx/2dLYFmy0
|
||||
wxvJHbZIhAaFbOeCvHg1ucIAQznli2jOr2qoB+yKRRPAp/3NXnZg1v7ce2CkwiAD52wjtC
|
||||
kAAAdILMJUeyzCVHsAAAAHc3NoLXJzYQAAAgEAvi26NDQ8cYTTztqPe9ZgF5HR/rIo5FoD
|
||||
gL5NbbZKW6h0txP9Fd8sid9Bgmo+aGkeM327tPVVMQ6UFmdRksOCIDWQDjNLF8b6S+Fu95
|
||||
tvMKSbGreRoR32OvgZKVI6KmOsF4z4GIv9naPplZswtKEUhSSI+/gPdAs9wfwalqZ77e82
|
||||
QJ727bYMeC3lzuoT+KBIdYufJ4OQhLtpHrqhB5sn7s6+oCv/u85GSln5SIC18Hi2t9lW4t
|
||||
gb5tH8P0kEDDWGfPS5okqGi4kFTvwBXOCS2r4dhi5hRkpP7PqG4np0OCfvK5IRRJ27fCnj
|
||||
0loc+puZJAxnPMbuJr64vwxRx78PM/V0PDUsl0P6aR/vbe0XmF9FGqbWf2Tar1p4r6C9/b
|
||||
MzcDz8seYT8hzLIHP3+Rl1hdlsTLm+1EzhaExKId+tjXegKGG4nU24h6qHEnRxLQDMwEsd
|
||||
kfj4E1pVypZJXVyNj99Do84vi0EUnu7R4HmQb/C+Pu7qMDtLT3Zk7O5Mg4LQ+cTz9V0noY
|
||||
EAyG46nAB4U/nJzBnV1J+aAdpioHmUAYhLYlQ9Kiy7LCJi92g9Wqa4wxMKxBbO5ZeH++p2
|
||||
p2lUi/oQNqx/2dLYFmy0wxvJHbZIhAaFbOeCvHg1ucIAQznli2jOr2qoB+yKRRPAp/3NXn
|
||||
Zg1v7ce2CkwiAD52wjtCkAAAADAQABAAACAQCbnVsyAFQ9J00Rg/HIiUATyTQlzq57O9SF
|
||||
8jH1RiZOHedzLx32WaleH5rBFiJ+2RTnWUjQ57aP77fpJR2wk93UcT+w/vPBPwXsNUjRvx
|
||||
Qan3ZzRCYbyiKDWiNslmYV7X0RwD36CAK8jTVDP7t48h2SXLTiSLaMY+5i3uD6yLu7k/O2
|
||||
qNyw4jgN1rCmwQ8acD0aQec3NAZ7NcbsaBX/3Uutsup0scwOZtlJWZoLY5Z8cKpCgcsAz4
|
||||
j1NHnNZvey7dFgSffj/ktdvf7kBH0w/GnuJ4aNF0Jte70u0kiw5TZYBQVFh74tgUu6a6SJ
|
||||
qUbxIYUL5EJNjxGsDn+phHEemw3aMv0CwZG6Tqaionlna7bLsl9Bg1HTGclczVWx8uqC+M
|
||||
6agLmkhYCHG0rVj8h5smjXAQXtmvIDVYDOlJZZoF9VAOCj6QfmJUH1NAGpCs1HDHbeOxGA
|
||||
OLCh4d3F4rScPqhGdtSt4W13VFIvXn2Qqoz9ufepZsee1SZqpcerxywx2wN9ZAzu+X8lTN
|
||||
i+TA2B3vWpqqucOEsp4JwDN+VMKZqKUGUDWcm/eHSaG6wq0q734LUlgM85TjaIg8QsNtWV
|
||||
giB1nWwsYIuH4rsFNFGEwURYdGBcw6idH0GZ7I4RaIB5F9oOza1d601E0APHYrtnx9yOiK
|
||||
nOtJ+5ZmVZovaDRfu1aQAAAQBU/EFaNUzoVhO04pS2L6BlByt963bOIsSJhdlEzek5AAli
|
||||
eaf1S/PD6xWCc0IGY+GZE0HPbhsKYanjqOpWldcA2T7fzf4oz4vFBfUkPYo/MLSlLCYsDd
|
||||
IH3wBkCssnfR5EkzNgxnOvq646Nl64BMvxwSIXGPktdq9ZALxViwricSRzCFURnh5vLHWU
|
||||
wBzSgAA0UlZ9E64GtAv066+AoZCp83GhTLRC4o0naE2e/K4op4BCFHLrZ8eXmDRK3NJj80
|
||||
Vkn+uhrk+SHmbjIhmS57Pv9p8TWyRvemph/nMUuZGKBUu2X+JQxggck0KigIrXjsmciCsM
|
||||
BIM3mYDDfjYbyVhTAAABAQDkV8O1bWUsAIqk7RU+iDZojN5kaO+zUvj1TafX8QX1sY6pu4
|
||||
Z2cfSEka1532BaehM95bQm7BCPw4cYg56XidmCQTZ9WaWqxVrOo48EKXUtZMZx6nKFOKlq
|
||||
MT2XTMnGT9n7kFCfEjSVkAjuJ9ZTFLOaoXAaVRnxeHQwOKaup5KKP9GSzNIw328U+96s3V
|
||||
WKHeT4pMjHBccgW/qX/tRRidZw5in5uBC9Ew5y3UACFTkNOnhUwVfyUNbBZJ2W36msQ3KD
|
||||
AN7nOrQHqhd3NFyCEy2ovIAKVBacr/VEX6EsRUshIehJzz8EY9f3kXL7WT2QDoz2giPeBJ
|
||||
HJdEpXt43UpszjAAABAQDVNpqNdHUlCs9XnbIvc6ZRrNh79wt65YFfvh/QEuA33KnA6Ri6
|
||||
EgnV5IdUWXS/UFaYcm2udydrBpVIVifSYl3sioHBylpri23BEy38PKwVXvghUtfpN6dWGn
|
||||
NZUG25fQPtIzqi+lo953ZjIj+Adi17AeVv4P4NiLrZeM9lXfWf2pEPOecxXs1IwAf9IiDQ
|
||||
WepAwRLsu42eEnHA+DSJPZUkSbISfM5X345k0g6EVATX/yLL3CsqClPzPtsqjh6rbEfFg3
|
||||
2OfIMcWV77gOlGWGQ+bUHc8kV6xJqV9QVacLWzfLvIqHF0wQMf8WLOVHEzkfiq4VjwhVqr
|
||||
/+FFvljm5nSDAAAAEW1pa2VAQzAyWTUwVEdKR0g4AQ==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC+Lbo0NDxxhNPO2o971mAXkdH+sijkWgOAvk1ttkpbqHS3E/0V3yyJ30GCaj5oaR4zfbu09VUxDpQWZ1GSw4IgNZAOM0sXxvpL4W73m28wpJsat5GhHfY6+BkpUjoqY6wXjPgYi/2do+mVmzC0oRSFJIj7+A90Cz3B/BqWpnvt7zZAnvbttgx4LeXO6hP4oEh1i58ng5CEu2keuqEHmyfuzr6gK/+7zkZKWflIgLXweLa32Vbi2Bvm0fw/SQQMNYZ89LmiSoaLiQVO/AFc4JLavh2GLmFGSk/s+obienQ4J+8rkhFEnbt8KePSWhz6m5kkDGc8xu4mvri/DFHHvw8z9XQ8NSyXQ/ppH+9t7ReYX0UaptZ/ZNqvWnivoL39szNwPPyx5hPyHMsgc/f5GXWF2WxMub7UTOFoTEoh362Nd6AoYbidTbiHqocSdHEtAMzASx2R+PgTWlXKlkldXI2P30Ojzi+LQRSe7tHgeZBv8L4+7uowO0tPdmTs7kyDgtD5xPP1XSehgQDIbjqcAHhT+cnMGdXUn5oB2mKgeZQBiEtiVD0qLLssImL3aD1aprjDEwrEFs7ll4f76nanaVSL+hA2rH/Z0tgWbLTDG8kdtkiEBoVs54K8eDW5wgBDOeWLaM6vaqgH7IpFE8Cn/c1edmDW/tx7YKTCIAPnbCO0KQ== mike@C02Y50TGJGH8
|
||||
@@ -0,0 +1,11 @@
|
||||
Host *
|
||||
AddressFamily inet
|
||||
|
||||
Host {{hostname}}
|
||||
ProxyCommand bash -c '/usr/local/bin/cloudflared access ssh-gen --hostname %h; ssh -F /root/.ssh/short_lived_cert_config -tt %r@cfpipe-{{hostname}} >&2 <&1'
|
||||
|
||||
Host cfpipe-{{hostname}}
|
||||
HostName {{hostname}}
|
||||
ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h
|
||||
IdentityFile ~/.cloudflared/{{hostname}}-cf_key
|
||||
CertificateFile ~/.cloudflared/{{hostname}}-cf_key-cert.pub
|
||||
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
Cloudflared Integration tests
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import subprocess
|
||||
import os
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
|
||||
from pexpect import pxssh
|
||||
|
||||
|
||||
class TestSSHBase(unittest.TestCase):
|
||||
"""
|
||||
SSH test base class containing constants and helper funcs
|
||||
"""
|
||||
|
||||
HOSTNAME = os.environ["SSH_HOSTNAME"]
|
||||
SSH_USER = os.environ["SSH_USER"]
|
||||
SSH_TARGET = f"{SSH_USER}@{HOSTNAME}"
|
||||
AUTHORIZED_KEYS_SSH_CONFIG = os.environ["AUTHORIZED_KEYS_SSH_CONFIG"]
|
||||
SHORT_LIVED_CERT_SSH_CONFIG = os.environ["SHORT_LIVED_CERT_SSH_CONFIG"]
|
||||
SSH_OPTIONS = {"StrictHostKeyChecking": "no"}
|
||||
|
||||
@classmethod
|
||||
def get_ssh_command(cls, pty=True):
|
||||
"""
|
||||
Return ssh command arg list. If pty is true, a PTY is forced for the session.
|
||||
"""
|
||||
cmd = [
|
||||
"ssh",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-F",
|
||||
cls.AUTHORIZED_KEYS_SSH_CONFIG,
|
||||
cls.SSH_TARGET,
|
||||
]
|
||||
if not pty:
|
||||
cmd += ["-T"]
|
||||
else:
|
||||
cmd += ["-tt"]
|
||||
|
||||
return cmd
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def ssh_session_manager(cls, *args, **kwargs):
|
||||
"""
|
||||
Context manager for interacting with a pxssh session.
|
||||
Disables pty echo on the remote server and ensures session is terminated afterward.
|
||||
"""
|
||||
session = pxssh.pxssh(options=cls.SSH_OPTIONS)
|
||||
|
||||
session.login(
|
||||
cls.HOSTNAME,
|
||||
username=cls.SSH_USER,
|
||||
original_prompt=r"[#@$]",
|
||||
ssh_config=kwargs.get("ssh_config", cls.AUTHORIZED_KEYS_SSH_CONFIG),
|
||||
ssh_tunnels=kwargs.get("ssh_tunnels", {}),
|
||||
)
|
||||
try:
|
||||
session.sendline("stty -echo")
|
||||
session.prompt()
|
||||
yield session
|
||||
finally:
|
||||
session.logout()
|
||||
|
||||
@staticmethod
|
||||
def get_command_output(session, cmd):
|
||||
"""
|
||||
Executes command on remote ssh server and waits for prompt.
|
||||
Returns command output
|
||||
"""
|
||||
session.sendline(cmd)
|
||||
session.prompt()
|
||||
return session.before.decode().strip()
|
||||
|
||||
def exec_command(self, cmd, shell=False):
|
||||
"""
|
||||
Executes command locally. Raises Assertion error for non-zero return code.
|
||||
Returns stdout and stderr
|
||||
"""
|
||||
proc = subprocess.Popen(
|
||||
cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, shell=shell
|
||||
)
|
||||
raw_out, raw_err = proc.communicate()
|
||||
|
||||
out = raw_out.decode()
|
||||
err = raw_err.decode()
|
||||
self.assertEqual(proc.returncode, 0, msg=f"stdout: {out} stderr: {err}")
|
||||
return out.strip(), err.strip()
|
||||
|
||||
|
||||
class TestSSHCommandExec(TestSSHBase):
|
||||
"""
|
||||
Tests inline ssh command exec
|
||||
"""
|
||||
|
||||
# Name of file to be downloaded over SCP on remote server.
|
||||
REMOTE_SCP_FILENAME = os.environ["REMOTE_SCP_FILENAME"]
|
||||
|
||||
@classmethod
|
||||
def get_scp_base_command(cls):
|
||||
return [
|
||||
"scp",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-v",
|
||||
"-F",
|
||||
cls.AUTHORIZED_KEYS_SSH_CONFIG,
|
||||
]
|
||||
|
||||
@unittest.skip(
|
||||
"This creates files on the remote. Should be skipped until server is dockerized."
|
||||
)
|
||||
def test_verbose_scp_sink_mode(self):
|
||||
with tempfile.NamedTemporaryFile() as fl:
|
||||
self.exec_command(
|
||||
self.get_scp_base_command() + [fl.name, f"{self.SSH_TARGET}:"]
|
||||
)
|
||||
|
||||
def test_verbose_scp_source_mode(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||
self.exec_command(
|
||||
self.get_scp_base_command()
|
||||
+ [f"{self.SSH_TARGET}:{self.REMOTE_SCP_FILENAME}", tmpdirname]
|
||||
)
|
||||
local_filename = os.path.join(tmpdirname, self.REMOTE_SCP_FILENAME)
|
||||
|
||||
self.assertTrue(os.path.exists(local_filename))
|
||||
self.assertTrue(os.path.getsize(local_filename) > 0)
|
||||
|
||||
def test_pty_command(self):
|
||||
base_cmd = self.get_ssh_command()
|
||||
|
||||
out, _ = self.exec_command(base_cmd + ["whoami"])
|
||||
self.assertEqual(out.strip().lower(), self.SSH_USER.lower())
|
||||
|
||||
out, _ = self.exec_command(base_cmd + ["tty"])
|
||||
self.assertNotEqual(out, "not a tty")
|
||||
|
||||
def test_non_pty_command(self):
|
||||
base_cmd = self.get_ssh_command(pty=False)
|
||||
|
||||
out, _ = self.exec_command(base_cmd + ["whoami"])
|
||||
self.assertEqual(out.strip().lower(), self.SSH_USER.lower())
|
||||
|
||||
out, _ = self.exec_command(base_cmd + ["tty"])
|
||||
self.assertEqual(out, "not a tty")
|
||||
|
||||
|
||||
class TestSSHShell(TestSSHBase):
|
||||
"""
|
||||
Tests interactive SSH shell
|
||||
"""
|
||||
|
||||
# File path to a file on the remote server with root only read privileges.
|
||||
ROOT_ONLY_TEST_FILE_PATH = os.environ["ROOT_ONLY_TEST_FILE_PATH"]
|
||||
|
||||
def test_ssh_pty(self):
|
||||
with self.ssh_session_manager() as session:
|
||||
|
||||
# Test shell launched as correct user
|
||||
username = self.get_command_output(session, "whoami")
|
||||
self.assertEqual(username.lower(), self.SSH_USER.lower())
|
||||
|
||||
# Test USER env variable set
|
||||
user_var = self.get_command_output(session, "echo $USER")
|
||||
self.assertEqual(user_var.lower(), self.SSH_USER.lower())
|
||||
|
||||
# Test HOME env variable set to true user home.
|
||||
home_env = self.get_command_output(session, "echo $HOME")
|
||||
pwd = self.get_command_output(session, "pwd")
|
||||
self.assertEqual(pwd, home_env)
|
||||
|
||||
# Test shell launched in correct user home dir.
|
||||
self.assertIn(username, pwd)
|
||||
|
||||
# Ensure shell launched with correct user's permissions and privs.
|
||||
# Cant read root owned 0700 files.
|
||||
output = self.get_command_output(
|
||||
session, f"cat {self.ROOT_ONLY_TEST_FILE_PATH}"
|
||||
)
|
||||
self.assertIn("Permission denied", output)
|
||||
|
||||
def test_short_lived_cert_auth(self):
|
||||
with self.ssh_session_manager(
|
||||
ssh_config=self.SHORT_LIVED_CERT_SSH_CONFIG
|
||||
) as session:
|
||||
username = self.get_command_output(session, "whoami")
|
||||
self.assertEqual(username.lower(), self.SSH_USER.lower())
|
||||
|
||||
|
||||
unittest.main()
|
||||
+1
-1
@@ -18,9 +18,9 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
cfpath "github.com/cloudflare/cloudflared/cmd/cloudflared/path"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"gopkg.in/coreos/go-oidc.v1/jose"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
cfpath "github.com/cloudflare/cloudflared/cmd/cloudflared/path"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/coreos/go-oidc.v1/jose"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package sshlog
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
//empty manager implements the Manager but does nothing (for testing and to disable logging unless the logs are set)
|
||||
type emptyManager struct {
|
||||
}
|
||||
|
||||
type emptyWriteCloser struct {
|
||||
}
|
||||
|
||||
// NewEmptyManager creates a new instance of a log empty log manager that does nothing
|
||||
func NewEmptyManager() Manager {
|
||||
return &emptyManager{}
|
||||
}
|
||||
|
||||
func (m *emptyManager) NewLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
return &emptyWriteCloser{}, nil
|
||||
}
|
||||
|
||||
func (m *emptyManager) NewSessionLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
return &emptyWriteCloser{}, nil
|
||||
}
|
||||
|
||||
// emptyWriteCloser
|
||||
|
||||
func (w *emptyWriteCloser) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *emptyWriteCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Generate go.capnp.out with:
|
||||
# capnp compile -o- go.capnp > go.capnp.out
|
||||
# Must run inside this directory to preserve paths.
|
||||
|
||||
@0xd12a1c51fedd6c88;
|
||||
|
||||
annotation package(file) :Text;
|
||||
annotation import(file) :Text;
|
||||
annotation doc(struct, field, enum) :Text;
|
||||
annotation tag(enumerant) :Text;
|
||||
annotation notag(enumerant) :Void;
|
||||
annotation customtype(field) :Text;
|
||||
annotation name(struct, field, union, enum, enumerant, interface, method, param, annotation, const, group) :Text;
|
||||
|
||||
$package("capnp");
|
||||
@@ -0,0 +1,167 @@
|
||||
package sshlog
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
logTimeFormat = "2006-01-02T15-04-05.000"
|
||||
megabyte = 1024 * 1024
|
||||
defaultFileSizeLimit = 100 * megabyte
|
||||
)
|
||||
|
||||
// Logger will buffer and write events to disk
|
||||
type Logger struct {
|
||||
sync.Mutex
|
||||
filename string
|
||||
file *os.File
|
||||
writeBuffer *bufio.Writer
|
||||
logger *logrus.Logger
|
||||
flushInterval time.Duration
|
||||
maxFileSize int64
|
||||
done chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// NewLogger creates a Logger instance. A buffer is created that needs to be
|
||||
// drained and closed when the caller is finished, so instances should call
|
||||
// Close when finished with this Logger instance. Writes will be flushed to disk
|
||||
// every second (fsync). filename is the name of the logfile to be created. The
|
||||
// logger variable is a logrus that will log all i/o, filesystem error etc, that
|
||||
// that shouldn't end execution of the logger, but are useful to report to the
|
||||
// caller.
|
||||
func NewLogger(filename string, logger *logrus.Logger, flushInterval time.Duration, maxFileSize int64) (*Logger, error) {
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger can't be nil")
|
||||
}
|
||||
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l := &Logger{filename: filename,
|
||||
file: f,
|
||||
writeBuffer: bufio.NewWriter(f),
|
||||
logger: logger,
|
||||
flushInterval: flushInterval,
|
||||
maxFileSize: maxFileSize,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
go l.writer()
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// Writes to a log buffer. Implements the io.Writer interface.
|
||||
func (l *Logger) Write(p []byte) (n int, err error) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
return l.writeBuffer.Write(p)
|
||||
}
|
||||
|
||||
// Close drains anything left in the buffer and cleans up any resources still
|
||||
// in use.
|
||||
func (l *Logger) Close() error {
|
||||
l.once.Do(func() {
|
||||
close(l.done)
|
||||
})
|
||||
if err := l.write(); err != nil {
|
||||
return err
|
||||
}
|
||||
return l.file.Close()
|
||||
}
|
||||
|
||||
// writer is the run loop that handles draining the write buffer and syncing
|
||||
// data to disk.
|
||||
func (l *Logger) writer() {
|
||||
ticker := time.NewTicker(l.flushInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := l.write(); err != nil {
|
||||
l.logger.Errorln(err)
|
||||
}
|
||||
case <-l.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write does the actual system write calls to disk and does a rotation if the
|
||||
// file size limit has been reached. Since the rotation happens at the end,
|
||||
// the rotation is a soft limit (aka the file can be bigger than the max limit
|
||||
// because of the final buffer flush)
|
||||
func (l *Logger) write() error {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
|
||||
if l.writeBuffer.Buffered() <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := l.writeBuffer.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := l.file.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if l.shouldRotate() {
|
||||
return l.rotate()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldRotate checks to see if the current file should be rotated to a new
|
||||
// logfile.
|
||||
func (l *Logger) shouldRotate() bool {
|
||||
info, err := l.file.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return info.Size() >= l.maxFileSize
|
||||
}
|
||||
|
||||
// rotate creates a new logfile with the existing filename and renames the
|
||||
// existing file with a current timestamp.
|
||||
func (l *Logger) rotate() error {
|
||||
if err := l.file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// move the existing file
|
||||
newname := rotationName(l.filename)
|
||||
if err := os.Rename(l.filename, newname); err != nil {
|
||||
return fmt.Errorf("can't rename log file: %s", err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(l.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open new logfile %s", err)
|
||||
}
|
||||
l.file = f
|
||||
l.writeBuffer = bufio.NewWriter(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
// rotationName creates a new filename from the given name, inserting a timestamp
|
||||
// between the filename and the extension.
|
||||
func rotationName(name string) string {
|
||||
dir := filepath.Dir(name)
|
||||
filename := filepath.Base(name)
|
||||
ext := filepath.Ext(filename)
|
||||
prefix := filename[:len(filename)-len(ext)]
|
||||
t := time.Now()
|
||||
timestamp := t.Format(logTimeFormat)
|
||||
return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext))
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package sshlog
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const logFileName = "test-logger.log"
|
||||
|
||||
func createLogger(t *testing.T) *Logger {
|
||||
os.Remove(logFileName)
|
||||
l := logrus.New()
|
||||
logger, err := NewLogger(logFileName, l, time.Millisecond, 1024)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the logger!", err)
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
testStr := "hi"
|
||||
logger := createLogger(t)
|
||||
defer func() {
|
||||
logger.Close()
|
||||
os.Remove(logFileName)
|
||||
}()
|
||||
|
||||
logger.Write([]byte(testStr))
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
data, err := ioutil.ReadFile(logFileName)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't read the log file!", err)
|
||||
}
|
||||
checkStr := string(data)
|
||||
if checkStr != testStr {
|
||||
t.Fatal("file data doesn't match!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilenameRotation(t *testing.T) {
|
||||
newName := rotationName("dir/bob/acoolloggername.log")
|
||||
|
||||
dir := filepath.Dir(newName)
|
||||
if dir != "dir/bob" {
|
||||
t.Fatal("rotation name doesn't respect the directory filepath:", newName)
|
||||
}
|
||||
|
||||
filename := filepath.Base(newName)
|
||||
if !strings.HasPrefix(filename, "acoolloggername") {
|
||||
t.Fatal("rotation filename is wrong:", filename)
|
||||
}
|
||||
|
||||
ext := filepath.Ext(newName)
|
||||
if ext != ".log" {
|
||||
t.Fatal("rotation file extension is wrong:", ext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRotation(t *testing.T) {
|
||||
logger := createLogger(t)
|
||||
|
||||
for i := 0; i < 2000; i++ {
|
||||
logger.Write([]byte("a string for testing rotation\n"))
|
||||
}
|
||||
logger.Close()
|
||||
|
||||
count := 0
|
||||
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(info.Name(), "test-logger") {
|
||||
log.Println("deleting: ", path)
|
||||
os.Remove(path)
|
||||
count++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if count < 2 {
|
||||
t.Fatal("rotation didn't roll files:", count)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package sshlog
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Manager be managing logs bruh
|
||||
type Manager interface {
|
||||
NewLogger(string, *logrus.Logger) (io.WriteCloser, error)
|
||||
NewSessionLogger(string, *logrus.Logger) (io.WriteCloser, error)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
baseDirectory string
|
||||
}
|
||||
|
||||
// New creates a new instance of a log manager
|
||||
func New(baseDirectory string) Manager {
|
||||
return &manager{
|
||||
baseDirectory: baseDirectory,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) NewLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
return NewLogger(filepath.Join(m.baseDirectory, name), logger, time.Second, defaultFileSizeLimit)
|
||||
}
|
||||
|
||||
func (m *manager) NewSessionLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
return NewSessionLogger(filepath.Join(m.baseDirectory, name), logger, time.Second, defaultFileSizeLimit)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Go = import "go.capnp";
|
||||
@0x8f43375162194466;
|
||||
$Go.package("sshlog");
|
||||
$Go.import("github.com/cloudflare/cloudflared/sshlog");
|
||||
|
||||
struct SessionLog {
|
||||
timestamp @0 :Text;
|
||||
content @1 :Data;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Code generated by capnpc-go. DO NOT EDIT.
|
||||
|
||||
package sshlog
|
||||
|
||||
import (
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
text "zombiezen.com/go/capnproto2/encoding/text"
|
||||
schemas "zombiezen.com/go/capnproto2/schemas"
|
||||
)
|
||||
|
||||
type SessionLog struct{ capnp.Struct }
|
||||
|
||||
// SessionLog_TypeID is the unique identifier for the type SessionLog.
|
||||
const SessionLog_TypeID = 0xa13a07c504a5ab64
|
||||
|
||||
func NewSessionLog(s *capnp.Segment) (SessionLog, error) {
|
||||
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
|
||||
return SessionLog{st}, err
|
||||
}
|
||||
|
||||
func NewRootSessionLog(s *capnp.Segment) (SessionLog, error) {
|
||||
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
|
||||
return SessionLog{st}, err
|
||||
}
|
||||
|
||||
func ReadRootSessionLog(msg *capnp.Message) (SessionLog, error) {
|
||||
root, err := msg.RootPtr()
|
||||
return SessionLog{root.Struct()}, err
|
||||
}
|
||||
|
||||
func (s SessionLog) String() string {
|
||||
str, _ := text.Marshal(0xa13a07c504a5ab64, s.Struct)
|
||||
return str
|
||||
}
|
||||
|
||||
func (s SessionLog) Timestamp() (string, error) {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.Text(), err
|
||||
}
|
||||
|
||||
func (s SessionLog) HasTimestamp() bool {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.IsValid() || err != nil
|
||||
}
|
||||
|
||||
func (s SessionLog) TimestampBytes() ([]byte, error) {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.TextBytes(), err
|
||||
}
|
||||
|
||||
func (s SessionLog) SetTimestamp(v string) error {
|
||||
return s.Struct.SetText(0, v)
|
||||
}
|
||||
|
||||
func (s SessionLog) Content() ([]byte, error) {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return []byte(p.Data()), err
|
||||
}
|
||||
|
||||
func (s SessionLog) HasContent() bool {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return p.IsValid() || err != nil
|
||||
}
|
||||
|
||||
func (s SessionLog) SetContent(v []byte) error {
|
||||
return s.Struct.SetData(1, v)
|
||||
}
|
||||
|
||||
// SessionLog_List is a list of SessionLog.
|
||||
type SessionLog_List struct{ capnp.List }
|
||||
|
||||
// NewSessionLog creates a new list of SessionLog.
|
||||
func NewSessionLog_List(s *capnp.Segment, sz int32) (SessionLog_List, error) {
|
||||
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz)
|
||||
return SessionLog_List{l}, err
|
||||
}
|
||||
|
||||
func (s SessionLog_List) At(i int) SessionLog { return SessionLog{s.List.Struct(i)} }
|
||||
|
||||
func (s SessionLog_List) Set(i int, v SessionLog) error { return s.List.SetStruct(i, v.Struct) }
|
||||
|
||||
func (s SessionLog_List) String() string {
|
||||
str, _ := text.MarshalList(0xa13a07c504a5ab64, s.List)
|
||||
return str
|
||||
}
|
||||
|
||||
// SessionLog_Promise is a wrapper for a SessionLog promised by a client call.
|
||||
type SessionLog_Promise struct{ *capnp.Pipeline }
|
||||
|
||||
func (p SessionLog_Promise) Struct() (SessionLog, error) {
|
||||
s, err := p.Pipeline.Struct()
|
||||
return SessionLog{s}, err
|
||||
}
|
||||
|
||||
const schema_8f43375162194466 = "x\xda\x120q`\x12d\x8dg`\x08dae\xfb" +
|
||||
"\x9f\xb2z)\xcbQv\xab\x85\x0c\x82B\x8c\xff\xd3\\" +
|
||||
"$\x93\x02\xcd\x9d\xfb\x19X\x99\xd8\x19\x18\x04E_\x09" +
|
||||
"*\x82h\xd9r\x06\xc6\xff\xc5\xa9\xc5\xc5\x99\xf9y\xf1" +
|
||||
"L9\xf9\xe9z\xc9\x89\x05y\x05V\xc1`!\xfe<" +
|
||||
"\x9f\xfc\xf4\x00F\xc6@\x0ef\x16\x06\x06\x16F\x06\x06" +
|
||||
"A\xcd \x06\x86@\x0df\xc6@\x13&FAFF" +
|
||||
"\x11F\x90\xa0\xa1\x13\x03C\xa0\x0e3c\xa0\x05\x13\xe3" +
|
||||
"\xff\x92\xcc\xdc\xd4\xe2\x92\xc4\\\x06\xc6\x02F\x1e\x06&" +
|
||||
"F\x1e\x06\xc6\xfa\xe4\xfc\xbc\x92\xd4\xbc\x12F^\x06&" +
|
||||
"F^\x06F@\x00\x00\x00\xff\xff\xdaK$\x1a"
|
||||
|
||||
func init() {
|
||||
schemas.Register(schema_8f43375162194466,
|
||||
0xa13a07c504a5ab64)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package sshlog
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
"zombiezen.com/go/capnproto2/pogs"
|
||||
)
|
||||
|
||||
// SessionLogger will buffer and write events to disk using capnp proto for session replay
|
||||
type SessionLogger struct {
|
||||
logger *Logger
|
||||
encoder *capnp.Encoder
|
||||
}
|
||||
|
||||
type sessionLogData struct {
|
||||
Timestamp string // The UTC timestamp of when the log occurred
|
||||
Content []byte // The shell output
|
||||
}
|
||||
|
||||
// NewSessionLogger creates a new session logger by encapsulating a Logger object and writing capnp encoded messages to it
|
||||
func NewSessionLogger(filename string, logger *logrus.Logger, flushInterval time.Duration, maxFileSize int64) (*SessionLogger, error) {
|
||||
l, err := NewLogger(filename, logger, flushInterval, maxFileSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionLogger := &SessionLogger{
|
||||
logger: l,
|
||||
encoder: capnp.NewEncoder(l),
|
||||
}
|
||||
return sessionLogger, nil
|
||||
}
|
||||
|
||||
// Writes to a log buffer. Implements the io.Writer interface.
|
||||
func (l *SessionLogger) Write(p []byte) (n int, err error) {
|
||||
return l.writeSessionLog(&sessionLogData{
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
Content: p,
|
||||
})
|
||||
}
|
||||
|
||||
// Close drains anything left in the buffer and cleans up any resources still
|
||||
// in use.
|
||||
func (l *SessionLogger) Close() error {
|
||||
return l.logger.Close()
|
||||
}
|
||||
|
||||
func (l *SessionLogger) writeSessionLog(p *sessionLogData) (int, error) {
|
||||
msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
log, err := NewRootSessionLog(seg)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
log.SetTimestamp(p.Timestamp)
|
||||
log.SetContent(p.Content)
|
||||
|
||||
if err := l.encoder.Encode(msg); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p.Content), nil
|
||||
}
|
||||
|
||||
func unmarshalSessionLog(s SessionLog) (*sessionLogData, error) {
|
||||
p := new(sessionLogData)
|
||||
err := pogs.Extract(p, SessionLog_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package sshlog
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
)
|
||||
|
||||
const sessionLogFileName = "test-session-logger.log"
|
||||
|
||||
func createSessionLogger(t *testing.T) *SessionLogger {
|
||||
os.Remove(sessionLogFileName)
|
||||
l := logrus.New()
|
||||
logger, err := NewSessionLogger(sessionLogFileName, l, time.Millisecond, 1024)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the logger!", err)
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
func TestSessionLogWrite(t *testing.T) {
|
||||
testStr := "hi"
|
||||
logger := createSessionLogger(t)
|
||||
defer func() {
|
||||
logger.Close()
|
||||
os.Remove(sessionLogFileName)
|
||||
}()
|
||||
|
||||
logger.Write([]byte(testStr))
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
f, err := os.Open(sessionLogFileName)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't read the log file!", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
msg, err := capnp.NewDecoder(f).Decode()
|
||||
if err != nil {
|
||||
t.Fatal("couldn't read the capnp msg file!", err)
|
||||
}
|
||||
|
||||
sessionLog, err := ReadRootSessionLog(msg)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't read the session log from the msg!", err)
|
||||
}
|
||||
|
||||
timeStr, err := sessionLog.Timestamp()
|
||||
if err != nil {
|
||||
t.Fatal("couldn't read the Timestamp field!", err)
|
||||
}
|
||||
|
||||
_, terr := time.Parse(time.RFC3339, timeStr)
|
||||
if terr != nil {
|
||||
t.Fatal("couldn't parse the Timestamp into the expected RFC3339 format", terr)
|
||||
}
|
||||
|
||||
data, err := sessionLog.Content()
|
||||
if err != nil {
|
||||
t.Fatal("couldn't read the Content field!", err)
|
||||
}
|
||||
|
||||
checkStr := string(data)
|
||||
if checkStr != testStr {
|
||||
t.Fatal("file data doesn't match!")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//+build !windows
|
||||
|
||||
package sshserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/pkg/errors"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
systemConfigPath = "/etc/cloudflared/"
|
||||
authorizedKeysDir = ".cloudflared/authorized_keys"
|
||||
)
|
||||
|
||||
func (s *SSHServer) configureAuthentication() {
|
||||
caCert, err := getCACert()
|
||||
if err != nil {
|
||||
s.logger.Info(err)
|
||||
}
|
||||
s.caCert = caCert
|
||||
s.PublicKeyHandler = s.authenticationHandler
|
||||
}
|
||||
|
||||
// authenticationHandler is a callback that returns true if the user attempting to connect is authenticated.
|
||||
func (s *SSHServer) authenticationHandler(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
sshUser, err := lookupUser(ctx.User())
|
||||
if err != nil {
|
||||
s.logger.Debugf("Invalid user: %s", ctx.User())
|
||||
return false
|
||||
}
|
||||
ctx.SetValue("sshUser", sshUser)
|
||||
|
||||
cert, ok := key.(*gossh.Certificate)
|
||||
if !ok {
|
||||
return s.authorizedKeyHandler(ctx, key)
|
||||
}
|
||||
return s.shortLivedCertHandler(ctx, cert)
|
||||
}
|
||||
|
||||
func (s *SSHServer) authorizedKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
sshUser, ok := ctx.Value("sshUser").(*User)
|
||||
if !ok {
|
||||
s.logger.Error("Failed to retrieve user from context")
|
||||
return false
|
||||
}
|
||||
|
||||
authorizedKeysPath := path.Join(sshUser.HomeDir, authorizedKeysDir)
|
||||
if _, err := os.Stat(authorizedKeysPath); os.IsNotExist(err) {
|
||||
s.logger.Debugf("authorized_keys file %s not found", authorizedKeysPath)
|
||||
return false
|
||||
}
|
||||
|
||||
authorizedKeysBytes, err := ioutil.ReadFile(authorizedKeysPath)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Errorf("Failed to load authorized_keys %s", authorizedKeysPath)
|
||||
return false
|
||||
}
|
||||
|
||||
for len(authorizedKeysBytes) > 0 {
|
||||
// Skips invalid keys. Returns error if no valid keys remain.
|
||||
pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes)
|
||||
authorizedKeysBytes = rest
|
||||
if err != nil {
|
||||
s.logger.Errorf("Invalid key(s) found in %s", authorizedKeysPath)
|
||||
return false
|
||||
}
|
||||
|
||||
if ssh.KeysEqual(pubKey, key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
s.logger.Debugf("Matching public key not found in %s", authorizedKeysPath)
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SSHServer) shortLivedCertHandler(ctx ssh.Context, cert *gossh.Certificate) bool {
|
||||
if !ssh.KeysEqual(s.caCert, cert.SignatureKey) {
|
||||
s.logger.Debug("CA certificate does not match user certificate signer")
|
||||
return false
|
||||
}
|
||||
|
||||
checker := gossh.CertChecker{}
|
||||
if err := checker.CheckCert(ctx.User(), cert); err != nil {
|
||||
s.logger.Debug(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getCACert() (ssh.PublicKey, error) {
|
||||
caCertPath := path.Join(systemConfigPath, "ca.pub")
|
||||
caCertBytes, err := ioutil.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("Failed to load CA certificate %s", caCertPath))
|
||||
}
|
||||
caCert, _, _, _, err := ssh.ParseAuthorizedKey(caCertBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to parse CA Certificate")
|
||||
}
|
||||
|
||||
return caCert, nil
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
//+build !windows
|
||||
|
||||
package sshserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
testDir = "testdata"
|
||||
testUserKeyFilename = "id_rsa.pub"
|
||||
testCAFilename = "ca.pub"
|
||||
testOtherCAFilename = "other_ca.pub"
|
||||
testUserCertFilename = "id_rsa-cert.pub"
|
||||
)
|
||||
|
||||
var (
|
||||
logger, hook = test.NewNullLogger()
|
||||
mockUser = &User{Username: "testUser", HomeDir: testDir}
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
authorizedKeysDir = testUserKeyFilename
|
||||
logger.SetLevel(logrus.DebugLevel)
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestPublicKeyAuth_Success(t *testing.T) {
|
||||
context, cancel := newMockContext(mockUser)
|
||||
defer cancel()
|
||||
|
||||
sshServer := SSHServer{logger: logger}
|
||||
|
||||
pubKey := getKey(t, testUserKeyFilename)
|
||||
assert.True(t, sshServer.authorizedKeyHandler(context, pubKey))
|
||||
}
|
||||
|
||||
func TestPublicKeyAuth_MissingKey(t *testing.T) {
|
||||
context, cancel := newMockContext(mockUser)
|
||||
defer cancel()
|
||||
|
||||
sshServer := SSHServer{logger: logger}
|
||||
|
||||
pubKey := getKey(t, testOtherCAFilename)
|
||||
assert.False(t, sshServer.authorizedKeyHandler(context, pubKey))
|
||||
assert.Contains(t, hook.LastEntry().Message, "Matching public key not found in")
|
||||
}
|
||||
|
||||
func TestPublicKeyAuth_InvalidUser(t *testing.T) {
|
||||
context, cancel := newMockContext(&User{Username: "notAUser"})
|
||||
defer cancel()
|
||||
|
||||
sshServer := SSHServer{logger: logger}
|
||||
|
||||
pubKey := getKey(t, testUserKeyFilename)
|
||||
assert.False(t, sshServer.authenticationHandler(context, pubKey))
|
||||
assert.Contains(t, hook.LastEntry().Message, "Invalid user")
|
||||
}
|
||||
|
||||
func TestPublicKeyAuth_MissingFile(t *testing.T) {
|
||||
tempUser, err := user.Current()
|
||||
require.Nil(t, err)
|
||||
currentUser, err := lookupUser(tempUser.Username)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Nil(t, err)
|
||||
context, cancel := newMockContext(currentUser)
|
||||
defer cancel()
|
||||
|
||||
sshServer := SSHServer{Server: ssh.Server{}, logger: logger}
|
||||
|
||||
pubKey := getKey(t, testUserKeyFilename)
|
||||
assert.False(t, sshServer.authorizedKeyHandler(context, pubKey))
|
||||
assert.Contains(t, hook.LastEntry().Message, "not found")
|
||||
}
|
||||
|
||||
func TestShortLivedCerts_Success(t *testing.T) {
|
||||
context, cancel := newMockContext(mockUser)
|
||||
defer cancel()
|
||||
|
||||
caCert := getKey(t, testCAFilename)
|
||||
sshServer := SSHServer{logger: log.CreateLogger(), caCert: caCert}
|
||||
|
||||
userCert, ok := getKey(t, testUserCertFilename).(*gossh.Certificate)
|
||||
require.True(t, ok)
|
||||
assert.True(t, sshServer.shortLivedCertHandler(context, userCert))
|
||||
}
|
||||
|
||||
func TestShortLivedCerts_CAsDontMatch(t *testing.T) {
|
||||
context, cancel := newMockContext(mockUser)
|
||||
defer cancel()
|
||||
|
||||
caCert := getKey(t, testOtherCAFilename)
|
||||
sshServer := SSHServer{logger: logger, caCert: caCert}
|
||||
|
||||
userCert, ok := getKey(t, testUserCertFilename).(*gossh.Certificate)
|
||||
require.True(t, ok)
|
||||
assert.False(t, sshServer.shortLivedCertHandler(context, userCert))
|
||||
assert.Equal(t, "CA certificate does not match user certificate signer", hook.LastEntry().Message)
|
||||
}
|
||||
|
||||
func TestShortLivedCerts_InvalidPrincipal(t *testing.T) {
|
||||
context, cancel := newMockContext(&User{Username: "NotAUser"})
|
||||
defer cancel()
|
||||
|
||||
caCert := getKey(t, testCAFilename)
|
||||
sshServer := SSHServer{logger: logger, caCert: caCert}
|
||||
|
||||
userCert, ok := getKey(t, testUserCertFilename).(*gossh.Certificate)
|
||||
require.True(t, ok)
|
||||
assert.False(t, sshServer.shortLivedCertHandler(context, userCert))
|
||||
assert.Contains(t, hook.LastEntry().Message, "not in the set of valid principals for given certificate")
|
||||
}
|
||||
|
||||
func getKey(t *testing.T, filename string) ssh.PublicKey {
|
||||
path := path.Join(testDir, filename)
|
||||
bytes, err := ioutil.ReadFile(path)
|
||||
require.Nil(t, err)
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(bytes)
|
||||
require.Nil(t, err)
|
||||
return pubKey
|
||||
}
|
||||
|
||||
type mockSSHContext struct {
|
||||
context.Context
|
||||
*sync.Mutex
|
||||
}
|
||||
|
||||
func newMockContext(user *User) (*mockSSHContext, context.CancelFunc) {
|
||||
innerCtx, cancel := context.WithCancel(context.Background())
|
||||
mockCtx := &mockSSHContext{innerCtx, &sync.Mutex{}}
|
||||
mockCtx.SetValue("sshUser", user)
|
||||
|
||||
// This naming is confusing but we cant change it because this mocks the SSHContext struct in gliderlabs/ssh
|
||||
mockCtx.SetValue("user", user.Username)
|
||||
return mockCtx, cancel
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) SetValue(key, value interface{}) {
|
||||
ctx.Context = context.WithValue(ctx.Context, key, value)
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) User() string {
|
||||
return ctx.Value("user").(string)
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) SessionID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) ClientVersion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) ServerVersion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) RemoteAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) LocalAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) Permissions() *ssh.Permissions {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// Taken from https://github.com/golang/go/blob/ad644d2e86bab85787879d41c2d2aebbd7c57db8/src/os/user/user.go
|
||||
// and modified to return login shell in User struct. cloudflared requires cgo for compilation because of this addition.
|
||||
|
||||
// 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.
|
||||
|
||||
// +build aix darwin dragonfly freebsd !android,linux netbsd openbsd solaris
|
||||
// +build cgo,!osusergo
|
||||
|
||||
package sshserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo solaris CFLAGS: -D_POSIX_PTHREAD_SEMANTICS
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <pwd.h>
|
||||
#include <grp.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static int mygetpwuid_r(int uid, struct passwd *pwd,
|
||||
char *buf, size_t buflen, struct passwd **result) {
|
||||
return getpwuid_r(uid, pwd, buf, buflen, result);
|
||||
}
|
||||
|
||||
static int mygetpwnam_r(const char *name, struct passwd *pwd,
|
||||
char *buf, size_t buflen, struct passwd **result) {
|
||||
return getpwnam_r(name, pwd, buf, buflen, result);
|
||||
}
|
||||
|
||||
static int mygetgrgid_r(int gid, struct group *grp,
|
||||
char *buf, size_t buflen, struct group **result) {
|
||||
return getgrgid_r(gid, grp, buf, buflen, result);
|
||||
}
|
||||
|
||||
static int mygetgrnam_r(const char *name, struct group *grp,
|
||||
char *buf, size_t buflen, struct group **result) {
|
||||
return getgrnam_r(name, grp, buf, buflen, result);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
type UnknownUserIdError int
|
||||
|
||||
func (e UnknownUserIdError) Error() string {
|
||||
return "user: unknown userid " + strconv.Itoa(int(e))
|
||||
}
|
||||
|
||||
// UnknownUserError is returned by Lookup when
|
||||
// a user cannot be found.
|
||||
type UnknownUserError string
|
||||
|
||||
func (e UnknownUserError) Error() string {
|
||||
return "user: unknown user " + string(e)
|
||||
}
|
||||
|
||||
// UnknownGroupIdError is returned by LookupGroupId when
|
||||
// a group cannot be found.
|
||||
type UnknownGroupIdError string
|
||||
|
||||
func (e UnknownGroupIdError) Error() string {
|
||||
return "group: unknown groupid " + string(e)
|
||||
}
|
||||
|
||||
// UnknownGroupError is returned by LookupGroup when
|
||||
// a group cannot be found.
|
||||
type UnknownGroupError string
|
||||
|
||||
func (e UnknownGroupError) Error() string {
|
||||
return "group: unknown group " + string(e)
|
||||
}
|
||||
|
||||
type User struct {
|
||||
// Uid is the user ID.
|
||||
// On POSIX systems, this is a decimal number representing the uid.
|
||||
// On Windows, this is a security identifier (SID) in a string format.
|
||||
// On Plan 9, this is the contents of /dev/user.
|
||||
Uid string
|
||||
// Gid is the primary group ID.
|
||||
// On POSIX systems, this is a decimal number representing the gid.
|
||||
// On Windows, this is a SID in a string format.
|
||||
// On Plan 9, this is the contents of /dev/user.
|
||||
Gid string
|
||||
// Username is the login name.
|
||||
Username string
|
||||
// Name is the user's real or display name.
|
||||
// It might be blank.
|
||||
// On POSIX systems, this is the first (or only) entry in the GECOS field
|
||||
// list.
|
||||
// On Windows, this is the user's display name.
|
||||
// On Plan 9, this is the contents of /dev/user.
|
||||
Name string
|
||||
// HomeDir is the path to the user's home directory (if they have one).
|
||||
HomeDir string
|
||||
|
||||
/****************** Begin added code ******************/
|
||||
// Login shell
|
||||
Shell string
|
||||
/****************** End added code ******************/
|
||||
}
|
||||
|
||||
func lookupUser(username string) (*User, error) {
|
||||
var pwd C.struct_passwd
|
||||
var result *C.struct_passwd
|
||||
nameC := make([]byte, len(username)+1)
|
||||
copy(nameC, username)
|
||||
|
||||
buf := alloc(userBuffer)
|
||||
defer buf.free()
|
||||
|
||||
err := retryWithBuffer(buf, func() syscall.Errno {
|
||||
// mygetpwnam_r is a wrapper around getpwnam_r to avoid
|
||||
// passing a size_t to getpwnam_r, because for unknown
|
||||
// reasons passing a size_t to getpwnam_r doesn't work on
|
||||
// Solaris.
|
||||
return syscall.Errno(C.mygetpwnam_r((*C.char)(unsafe.Pointer(&nameC[0])),
|
||||
&pwd,
|
||||
(*C.char)(buf.ptr),
|
||||
C.size_t(buf.size),
|
||||
&result))
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user: lookup username %s: %v", username, err)
|
||||
}
|
||||
if result == nil {
|
||||
return nil, UnknownUserError(username)
|
||||
}
|
||||
return buildUser(&pwd), err
|
||||
}
|
||||
|
||||
func buildUser(pwd *C.struct_passwd) *User {
|
||||
u := &User{
|
||||
Uid: strconv.FormatUint(uint64(pwd.pw_uid), 10),
|
||||
Gid: strconv.FormatUint(uint64(pwd.pw_gid), 10),
|
||||
Username: C.GoString(pwd.pw_name),
|
||||
Name: C.GoString(pwd.pw_gecos),
|
||||
HomeDir: C.GoString(pwd.pw_dir),
|
||||
/****************** Begin added code ******************/
|
||||
Shell: C.GoString(pwd.pw_shell),
|
||||
/****************** End added code ******************/
|
||||
}
|
||||
// The pw_gecos field isn't quite standardized. Some docs
|
||||
// say: "It is expected to be a comma separated list of
|
||||
// personal data where the first item is the full name of the
|
||||
// user."
|
||||
if i := strings.Index(u.Name, ","); i >= 0 {
|
||||
u.Name = u.Name[:i]
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
type bufferKind C.int
|
||||
|
||||
const (
|
||||
userBuffer = bufferKind(C._SC_GETPW_R_SIZE_MAX)
|
||||
)
|
||||
|
||||
func (k bufferKind) initialSize() C.size_t {
|
||||
sz := C.sysconf(C.int(k))
|
||||
if sz == -1 {
|
||||
// DragonFly and FreeBSD do not have _SC_GETPW_R_SIZE_MAX.
|
||||
// Additionally, not all Linux systems have it, either. For
|
||||
// example, the musl libc returns -1.
|
||||
return 1024
|
||||
}
|
||||
if !isSizeReasonable(int64(sz)) {
|
||||
// Truncate. If this truly isn't enough, retryWithBuffer will error on the first run.
|
||||
return maxBufferSize
|
||||
}
|
||||
return C.size_t(sz)
|
||||
}
|
||||
|
||||
type memBuffer struct {
|
||||
ptr unsafe.Pointer
|
||||
size C.size_t
|
||||
}
|
||||
|
||||
func alloc(kind bufferKind) *memBuffer {
|
||||
sz := kind.initialSize()
|
||||
return &memBuffer{
|
||||
ptr: C.malloc(sz),
|
||||
size: sz,
|
||||
}
|
||||
}
|
||||
|
||||
func (mb *memBuffer) resize(newSize C.size_t) {
|
||||
mb.ptr = C.realloc(mb.ptr, newSize)
|
||||
mb.size = newSize
|
||||
}
|
||||
|
||||
func (mb *memBuffer) free() {
|
||||
C.free(mb.ptr)
|
||||
}
|
||||
|
||||
// retryWithBuffer repeatedly calls f(), increasing the size of the
|
||||
// buffer each time, until f succeeds, fails with a non-ERANGE error,
|
||||
// or the buffer exceeds a reasonable limit.
|
||||
func retryWithBuffer(buf *memBuffer, f func() syscall.Errno) error {
|
||||
for {
|
||||
errno := f()
|
||||
if errno == 0 {
|
||||
return nil
|
||||
} else if errno != syscall.ERANGE {
|
||||
return errno
|
||||
}
|
||||
newSize := buf.size * 2
|
||||
if !isSizeReasonable(int64(newSize)) {
|
||||
return fmt.Errorf("internal buffer exceeds %d bytes", maxBufferSize)
|
||||
}
|
||||
buf.resize(newSize)
|
||||
}
|
||||
}
|
||||
|
||||
const maxBufferSize = 1 << 20
|
||||
|
||||
func isSizeReasonable(sz int64) bool {
|
||||
return sz > 0 && sz <= maxBufferSize
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//+build !windows
|
||||
|
||||
package sshserver
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
rsaFilename = "ssh_host_rsa_key"
|
||||
ecdsaFilename = "ssh_host_ecdsa_key"
|
||||
)
|
||||
|
||||
func (s *SSHServer) configureHostKeys() error {
|
||||
if _, err := os.Stat(systemConfigPath); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(systemConfigPath, 0755); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Error creating %s directory", systemConfigPath))
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.configureHostKey(s.ensureECDSAKeyExists); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.configureHostKey(s.ensureRSAKeyExists); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SSHServer) configureHostKey(keyFunc func() (string, error)) error {
|
||||
path, err := keyFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.SetOption(ssh.HostKeyFile(path)); err != nil {
|
||||
return errors.Wrap(err, "Could not set SSH host key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SSHServer) ensureRSAKeyExists() (string, error) {
|
||||
keyPath := filepath.Join(systemConfigPath, rsaFilename)
|
||||
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Error generating RSA host key")
|
||||
}
|
||||
|
||||
privateKey := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
}
|
||||
|
||||
if err = writePrivateKey(keyPath, privateKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
s.logger.Debug("Created new RSA SSH host key: ", keyPath)
|
||||
}
|
||||
return keyPath, nil
|
||||
}
|
||||
|
||||
func (s *SSHServer) ensureECDSAKeyExists() (string, error) {
|
||||
keyPath := filepath.Join(systemConfigPath, ecdsaFilename)
|
||||
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Error generating ECDSA host key")
|
||||
}
|
||||
|
||||
keyBytes, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Error marshalling ECDSA key")
|
||||
}
|
||||
|
||||
privateKey := &pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: keyBytes,
|
||||
}
|
||||
|
||||
if err = writePrivateKey(keyPath, privateKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
s.logger.Debug("Created new ECDSA SSH host key: ", keyPath)
|
||||
}
|
||||
return keyPath, nil
|
||||
}
|
||||
|
||||
func writePrivateKey(keyPath string, privateKey *pem.Block) error {
|
||||
if err := ioutil.WriteFile(keyPath, pem.EncodeToMemory(privateKey), 0600); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Error writing host key to %s", keyPath))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
//+build !windows
|
||||
|
||||
package sshserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/cloudflare/cloudflared/sshlog"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
auditEventAuth = "auth"
|
||||
auditEventStart = "session_start"
|
||||
auditEventStop = "session_stop"
|
||||
auditEventExec = "exec"
|
||||
auditEventScp = "scp"
|
||||
auditEventResize = "resize"
|
||||
sshContextSessionID = "sessionID"
|
||||
sshContextEventLogger = "eventLogger"
|
||||
)
|
||||
|
||||
type auditEvent struct {
|
||||
Event string `json:"event,omitempty"`
|
||||
EventType string `json:"event_type,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Login string `json:"login,omitempty"`
|
||||
Datetime string `json:"datetime,omitempty"`
|
||||
IPAddress string `json:"ip_address,omitempty"`
|
||||
}
|
||||
|
||||
// SSHServer adds on to the ssh.Server of the gliderlabs package
|
||||
type SSHServer struct {
|
||||
ssh.Server
|
||||
logger *logrus.Logger
|
||||
shutdownC chan struct{}
|
||||
caCert ssh.PublicKey
|
||||
logManager sshlog.Manager
|
||||
}
|
||||
|
||||
// New creates a new SSHServer and configures its host keys and authenication by the data provided
|
||||
func New(logManager sshlog.Manager, logger *logrus.Logger, version, address string, shutdownC chan struct{}, idleTimeout, maxTimeout time.Duration, enablePortForwarding bool) (*SSHServer, error) {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if currentUser.Uid != "0" {
|
||||
return nil, errors.New("cloudflared SSH server needs to run as root")
|
||||
}
|
||||
|
||||
forwardHandler := &ssh.ForwardedTCPHandler{}
|
||||
sshServer := SSHServer{
|
||||
Server: ssh.Server{
|
||||
Addr: address,
|
||||
MaxTimeout: maxTimeout,
|
||||
IdleTimeout: idleTimeout,
|
||||
Version: fmt.Sprintf("SSH-2.0-Cloudflare-Access_%s_%s", version, runtime.GOOS),
|
||||
// Register SSH global Request handlers to respond to tcpip forwarding
|
||||
RequestHandlers: map[string]ssh.RequestHandler{
|
||||
"tcpip-forward": forwardHandler.HandleSSHRequest,
|
||||
"cancel-tcpip-forward": forwardHandler.HandleSSHRequest,
|
||||
},
|
||||
// Register SSH channel types
|
||||
ChannelHandlers: map[string]ssh.ChannelHandler{
|
||||
"session": ssh.DefaultSessionHandler,
|
||||
"direct-tcpip": ssh.DirectTCPIPHandler,
|
||||
},
|
||||
},
|
||||
logger: logger,
|
||||
shutdownC: shutdownC,
|
||||
logManager: logManager,
|
||||
}
|
||||
|
||||
// AUTH-2050: This is a temporary workaround of a timing issue in the tunnel muxer to allow further testing.
|
||||
// TODO: Remove this
|
||||
sshServer.ConnCallback = func(conn net.Conn) net.Conn {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
return conn
|
||||
}
|
||||
|
||||
if enablePortForwarding {
|
||||
sshServer.LocalPortForwardingCallback = allowForward
|
||||
sshServer.ReversePortForwardingCallback = allowForward
|
||||
}
|
||||
|
||||
if err := sshServer.configureHostKeys(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sshServer.configureAuthentication()
|
||||
|
||||
return &sshServer, nil
|
||||
}
|
||||
|
||||
// Start the SSH server listener to start handling SSH connections from clients
|
||||
func (s *SSHServer) Start() error {
|
||||
s.logger.Infof("Starting SSH server at %s", s.Addr)
|
||||
|
||||
go func() {
|
||||
<-s.shutdownC
|
||||
if err := s.Close(); err != nil {
|
||||
s.logger.WithError(err).Error("Cannot close SSH server")
|
||||
}
|
||||
}()
|
||||
|
||||
s.Handle(s.connectionHandler)
|
||||
return s.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *SSHServer) connectionHandler(session ssh.Session) {
|
||||
sessionUUID, err := uuid.NewRandom()
|
||||
|
||||
if err != nil {
|
||||
if _, err := io.WriteString(session, "Failed to generate session ID\n"); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to generate session ID: Failed to write to SSH session")
|
||||
}
|
||||
s.errorAndExit(session, "", nil)
|
||||
return
|
||||
}
|
||||
sessionID := sessionUUID.String()
|
||||
|
||||
eventLogger, err := s.logManager.NewLogger(fmt.Sprintf("%s-event.log", sessionID), s.logger)
|
||||
if err != nil {
|
||||
if _, err := io.WriteString(session, "Failed to create event log\n"); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to create event log: Failed to write to create event logger")
|
||||
}
|
||||
s.errorAndExit(session, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
sshContext, ok := session.Context().(ssh.Context)
|
||||
if !ok {
|
||||
s.logger.Error("Could not retrieve session context")
|
||||
s.errorAndExit(session, "", nil)
|
||||
}
|
||||
|
||||
sshContext.SetValue(sshContextSessionID, sessionID)
|
||||
sshContext.SetValue(sshContextEventLogger, eventLogger)
|
||||
|
||||
// Get uid and gid of user attempting to login
|
||||
sshUser, uidInt, gidInt, success := s.getSSHUser(session, eventLogger)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
// Spawn shell under user
|
||||
var cmd *exec.Cmd
|
||||
if session.RawCommand() != "" {
|
||||
cmd = exec.Command(sshUser.Shell, "-c", session.RawCommand())
|
||||
|
||||
event := auditEventExec
|
||||
if strings.HasPrefix(session.RawCommand(), "scp") {
|
||||
event = auditEventScp
|
||||
}
|
||||
s.logAuditEvent(session, event)
|
||||
} else {
|
||||
cmd = exec.Command(sshUser.Shell)
|
||||
s.logAuditEvent(session, auditEventStart)
|
||||
defer s.logAuditEvent(session, auditEventStop)
|
||||
}
|
||||
// Supplementary groups are not explicitly specified. They seem to be inherited by default.
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uidInt, Gid: gidInt}, Setsid: true}
|
||||
cmd.Env = append(cmd.Env, session.Environ()...)
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", sshUser.Username))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", sshUser.HomeDir))
|
||||
cmd.Dir = sshUser.HomeDir
|
||||
|
||||
var shellInput io.WriteCloser
|
||||
var shellOutput io.ReadCloser
|
||||
pr, pw := io.Pipe()
|
||||
defer pw.Close()
|
||||
|
||||
ptyReq, winCh, isPty := session.Pty()
|
||||
|
||||
if isPty {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
|
||||
tty, err := s.startPtySession(cmd, winCh, func() {
|
||||
s.logAuditEvent(session, auditEventResize)
|
||||
})
|
||||
shellInput = tty
|
||||
shellOutput = tty
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to start pty session")
|
||||
close(s.shutdownC)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var shellError io.ReadCloser
|
||||
shellInput, shellOutput, shellError, err = s.startNonPtySession(cmd)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to start non-pty session")
|
||||
close(s.shutdownC)
|
||||
return
|
||||
}
|
||||
|
||||
// Write stderr to both the command recorder, and remote user
|
||||
go func() {
|
||||
mw := io.MultiWriter(pw, session.Stderr())
|
||||
if _, err := io.Copy(mw, shellError); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to write stderr to user")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
sessionLogger, err := s.logManager.NewSessionLogger(fmt.Sprintf("%s-session.log", sessionID), s.logger)
|
||||
if err != nil {
|
||||
if _, err := io.WriteString(session, "Failed to create log\n"); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to create log: Failed to write to SSH session")
|
||||
}
|
||||
s.errorAndExit(session, "", nil)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer sessionLogger.Close()
|
||||
defer pr.Close()
|
||||
_, err := io.Copy(sessionLogger, pr)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to write session log")
|
||||
}
|
||||
}()
|
||||
|
||||
// Write stdin to shell
|
||||
go func() {
|
||||
|
||||
/*
|
||||
Only close shell stdin for non-pty sessions because they have distinct stdin, stdout, and stderr.
|
||||
This is done to prevent commands like SCP from hanging after all data has been sent.
|
||||
PTY sessions share one file for all three streams and the shell process closes it.
|
||||
Closing it here also closes shellOutput and causes an error on copy().
|
||||
*/
|
||||
if !isPty {
|
||||
defer shellInput.Close()
|
||||
}
|
||||
if _, err := io.Copy(shellInput, session); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to write incoming command to pty")
|
||||
}
|
||||
}()
|
||||
|
||||
// Write stdout to both the command recorder, and remote user
|
||||
mw := io.MultiWriter(pw, session)
|
||||
if _, err := io.Copy(mw, shellOutput); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to write stdout to user")
|
||||
}
|
||||
|
||||
// Wait for all resources associated with cmd to be released
|
||||
// Returns error if shell exited with a non-zero status or received a signal
|
||||
if err := cmd.Wait(); err != nil {
|
||||
s.logger.WithError(err).Debug("Shell did not close correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// getSSHUser gets the ssh user, uid, and gid of the user attempting to login
|
||||
func (s *SSHServer) getSSHUser(session ssh.Session, eventLogger io.WriteCloser) (*User, uint32, uint32, bool) {
|
||||
// Get uid and gid of user attempting to login
|
||||
sshUser, ok := session.Context().Value("sshUser").(*User)
|
||||
if !ok || sshUser == nil {
|
||||
s.errorAndExit(session, "Error retrieving credentials from session", nil)
|
||||
return nil, 0, 0, false
|
||||
}
|
||||
s.logAuditEvent(session, auditEventAuth)
|
||||
|
||||
uidInt, err := stringToUint32(sshUser.Uid)
|
||||
if err != nil {
|
||||
s.errorAndExit(session, "Invalid user", err)
|
||||
return sshUser, 0, 0, false
|
||||
}
|
||||
gidInt, err := stringToUint32(sshUser.Gid)
|
||||
if err != nil {
|
||||
s.errorAndExit(session, "Invalid user group", err)
|
||||
return sshUser, 0, 0, false
|
||||
}
|
||||
return sshUser, uidInt, gidInt, true
|
||||
}
|
||||
|
||||
// errorAndExit reports an error with the session and exits
|
||||
func (s *SSHServer) errorAndExit(session ssh.Session, errText string, err error) {
|
||||
if exitError := session.Exit(1); exitError != nil {
|
||||
s.logger.WithError(exitError).Error("Failed to close SSH session")
|
||||
} else if err != nil {
|
||||
s.logger.WithError(err).Error(errText)
|
||||
} else if errText != "" {
|
||||
s.logger.Error(errText)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSHServer) startNonPtySession(cmd *exec.Cmd) (stdin io.WriteCloser, stdout io.ReadCloser, stderr io.ReadCloser, err error) {
|
||||
stdin, err = cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
stdout, err = cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stderr, err = cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *SSHServer) startPtySession(cmd *exec.Cmd, winCh <-chan ssh.Window, logCallback func()) (io.ReadWriteCloser, error) {
|
||||
tty, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Handle terminal window size changes
|
||||
go func() {
|
||||
for win := range winCh {
|
||||
if errNo := setWinsize(tty, win.Width, win.Height); errNo != 0 {
|
||||
s.logger.WithError(err).Error("Failed to set pty window size")
|
||||
close(s.shutdownC)
|
||||
return
|
||||
}
|
||||
logCallback()
|
||||
}
|
||||
}()
|
||||
|
||||
return tty, nil
|
||||
}
|
||||
|
||||
func (s *SSHServer) logAuditEvent(session ssh.Session, eventType string) {
|
||||
username := "unknown"
|
||||
sshUser, ok := session.Context().Value("sshUser").(*User)
|
||||
if ok && sshUser != nil {
|
||||
username = sshUser.Username
|
||||
}
|
||||
|
||||
sessionID, ok := session.Context().Value(sshContextSessionID).(string)
|
||||
if !ok {
|
||||
s.logger.Error("Failed to retrieve sessionID from context")
|
||||
return
|
||||
}
|
||||
writer, ok := session.Context().Value(sshContextEventLogger).(io.WriteCloser)
|
||||
if !ok {
|
||||
s.logger.Error("Failed to retrieve eventLogger from context")
|
||||
return
|
||||
}
|
||||
|
||||
event := auditEvent{
|
||||
Event: session.RawCommand(),
|
||||
EventType: eventType,
|
||||
SessionID: sessionID,
|
||||
User: username,
|
||||
Login: username,
|
||||
Datetime: time.Now().UTC().Format(time.RFC3339),
|
||||
IPAddress: session.RemoteAddr().String(),
|
||||
}
|
||||
data, err := json.Marshal(&event)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to marshal audit event. malformed audit object")
|
||||
return
|
||||
}
|
||||
line := string(data) + "\n"
|
||||
if _, err := writer.Write([]byte(line)); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to write audit event.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Sets PTY window size for terminal
|
||||
func setWinsize(f *os.File, w, h int) syscall.Errno {
|
||||
_, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),
|
||||
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0})))
|
||||
return errNo
|
||||
}
|
||||
|
||||
func stringToUint32(str string) (uint32, error) {
|
||||
uid, err := strconv.ParseUint(str, 10, 32)
|
||||
return uint32(uid), err
|
||||
}
|
||||
|
||||
func allowForward(_ ssh.Context, _ string, _ uint32) bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//+build windows
|
||||
|
||||
package sshserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/sshlog"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type SSHServer struct{}
|
||||
|
||||
func New(_ sshlog.Manager, _ *logrus.Logger, _, _ string, _ chan struct{}, _, _ time.Duration, _ bool) (*SSHServer, error) {
|
||||
return nil, errors.New("cloudflared ssh server is not supported on windows")
|
||||
}
|
||||
|
||||
func (s *SSHServer) Start() error {
|
||||
return errors.New("cloudflared ssh server is not supported on windows")
|
||||
}
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAQEA0c6EklYvC9B041qEGWDNuot6G4tTVm9LCQC0vA+v2n25ru9CINV6
|
||||
8IljmXBORXBwfG6PdLhg0SEabZUbsNX5WrIVbGovcghKS6GRsqI5+Quhm+o8eG042JE/hB
|
||||
oYdZ19TcMEyPOGzHsx0U/BSN9ZJWVCxqN51iI6qyhz9f6jlX2LQBFEvXlhxgF3owBEf8UC
|
||||
Zt/UvbZdmeeyKNQElPmiVLIJEAPCueECp7a2mjCiP3zqjDvSeeGk4CelB/1qZZ4V2n7fvb
|
||||
HZjAB5JJs4KXs5o8KgvQnqgQMxiLFZ4PATt4+mxEzh4JymppbqJOo2rYwOA3TAIEWWtYRV
|
||||
/ZKJ0AyhhQAAA8gciO8XHIjvFwAAAAdzc2gtcnNhAAABAQDRzoSSVi8L0HTjWoQZYM26i3
|
||||
obi1NWb0sJALS8D6/afbmu70Ig1XrwiWOZcE5FcHB8bo90uGDRIRptlRuw1flashVsai9y
|
||||
CEpLoZGyojn5C6Gb6jx4bTjYkT+EGhh1nX1NwwTI84bMezHRT8FI31klZULGo3nWIjqrKH
|
||||
P1/qOVfYtAEUS9eWHGAXejAER/xQJm39S9tl2Z57Io1ASU+aJUsgkQA8K54QKntraaMKI/
|
||||
fOqMO9J54aTgJ6UH/WplnhXaft+9sdmMAHkkmzgpezmjwqC9CeqBAzGIsVng8BO3j6bETO
|
||||
HgnKamluok6jatjA4DdMAgRZa1hFX9konQDKGFAAAAAwEAAQAAAQEApVzGdKhk8ETevAst
|
||||
rurze6JPHcKUbr3NQE1EJi2fBvCtF0oQrtxTx54h2GAB8Q0MO6bQfsiL1ojm0ZQCfUBJBs
|
||||
jxxb9zoccS98Vilo7ybm5SdBcMjkZX1am1jCMdQCZfCpk4/kGi7yvyOe1IhG01UBodpX5X
|
||||
mwTjhN+fdjW7LSiW6cKPClN49CZKgmtvI27FCt+/TtMzdCXOiJxJ4yZCzCRhSgssV0gWI1
|
||||
0VJr/MHirKUvv/qCLAuOBxIr9UgdduRZUpNX+KS2rfhFEbjnUqc/57aAakpQmuPB5I+s9G
|
||||
DnrF0HSHpq7u1XC1SvYlnFBN/0A7Hw/MX2SaBFH7mc9AAQAAAIAFuTHr6O8tCvWEawfxC0
|
||||
qiAPQ+Yy1vthq5uewmuQujMutUnc9JAUl32PdU1DbS7APC1Dg9XL7SyAB6A+ZpRJRAKgCY
|
||||
SneAKE6hOytH+yM206aekrz6VuZiSpBqpfEqDibVAaZIO8sv/9dtZd6kWemxNErPQoKJey
|
||||
Z7/cuWUWQovAAAAIEA6ugIlVj1irPmElyCCt5YfPv2x8Dl54ELoP/WsffsrPHNQog64hFd
|
||||
ahD7Wq63TA566bN85fkx8OVU5TbbEQmkHgOEV6nDRY2YsBSqIOblA/KehtfdUIqZB0iNBh
|
||||
Gn6TV/z6HwnSR3gKv4b66Gveek6LfRAG3mbsLCgyRAbYgn6YUAAACBAOSlf+n1eh6yjtvF
|
||||
Zecq3Zslj7O8cUs17PQx4vQ7sXNCFrIZdevWPIn9sVrt7/hsTrXunDz6eXCeclB35KZe3H
|
||||
WPVjRoD+xnr5+sXx2qXOnKCR0LdFybso6IR5bXAI6DNSNfP7D9LPEQ+R73Jk0jPuLYzocS
|
||||
iM89KZiuGpzr01gBAAAAEW1pa2VAQzAyWTUwVEdKR0g4AQ==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDRzoSSVi8L0HTjWoQZYM26i3obi1NWb0sJALS8D6/afbmu70Ig1XrwiWOZcE5FcHB8bo90uGDRIRptlRuw1flashVsai9yCEpLoZGyojn5C6Gb6jx4bTjYkT+EGhh1nX1NwwTI84bMezHRT8FI31klZULGo3nWIjqrKHP1/qOVfYtAEUS9eWHGAXejAER/xQJm39S9tl2Z57Io1ASU+aJUsgkQA8K54QKntraaMKI/fOqMO9J54aTgJ6UH/WplnhXaft+9sdmMAHkkmzgpezmjwqC9CeqBAzGIsVng8BO3j6bETOHgnKamluok6jatjA4DdMAgRZa1hFX9konQDKGF mike@C02Y50TGJGH8
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAgEA60Kneo87qPsh+zErWFl7vx93c7fyTxbZ9lUNqafgXy/BLOCc/nQS
|
||||
McosVSLsQrbHlhYzfmZEhTiubmuYUrHchmsn1ml1HIqP8T5aDgtNbLqYnS4H5oO4Sj1+XH
|
||||
lQtU7n7zHXgca9SnMWt1Fhkx1mvkeiOKs0eq7hV2TuIZxfmbYfIVvJGwrL0uWzbSEE1gvx
|
||||
gTXZHxEChIQyrNviljgi4u2MD/cIi6KMeYUnaTL1FxO9G4GIFiy7ueHRwOZPIFHgYm+Vrt
|
||||
X7XafSF0///zCrC63zzWt/6A06hFepOz2VXvm7SdckaR7qMXAb7kipsc0+dKk9ggU7Fqpx
|
||||
ZY5cVeZo9RlRVhRXGDy7mABA/FMwvv+qYCgJ3nlZbdKbaiPLQu8ScTlJ9sMI06/ZiEY04b
|
||||
meZ0ASM52gaDGjrFbbnuHNf5XV/oreEUhtCrryFnoIxmKgHznGjZ55q77FtTHnrAKFmKFP
|
||||
11s3MLIX9o4RgtriOtl4KenkIfUumgtrwY/UGjOaOQUOrVH1am54wkUiVEF0Qd3AD8KCl/
|
||||
l/xT5+t6cOspZ9GIhwa2NBmRjN/wVGp+Yrb08Re3kxPCX9bs5iLe+kHN0vuFr7RDo+eUoi
|
||||
SPhWl6FUqx2W9NZqekmEgKn3oKrfbGaMH1VLkaKWlzQ4xJzP0iadQbIXGryLEYASydemZt
|
||||
sAAAdQ/ovjxf6L48UAAAAHc3NoLXJzYQAAAgEA60Kneo87qPsh+zErWFl7vx93c7fyTxbZ
|
||||
9lUNqafgXy/BLOCc/nQSMcosVSLsQrbHlhYzfmZEhTiubmuYUrHchmsn1ml1HIqP8T5aDg
|
||||
tNbLqYnS4H5oO4Sj1+XHlQtU7n7zHXgca9SnMWt1Fhkx1mvkeiOKs0eq7hV2TuIZxfmbYf
|
||||
IVvJGwrL0uWzbSEE1gvxgTXZHxEChIQyrNviljgi4u2MD/cIi6KMeYUnaTL1FxO9G4GIFi
|
||||
y7ueHRwOZPIFHgYm+VrtX7XafSF0///zCrC63zzWt/6A06hFepOz2VXvm7SdckaR7qMXAb
|
||||
7kipsc0+dKk9ggU7FqpxZY5cVeZo9RlRVhRXGDy7mABA/FMwvv+qYCgJ3nlZbdKbaiPLQu
|
||||
8ScTlJ9sMI06/ZiEY04bmeZ0ASM52gaDGjrFbbnuHNf5XV/oreEUhtCrryFnoIxmKgHznG
|
||||
jZ55q77FtTHnrAKFmKFP11s3MLIX9o4RgtriOtl4KenkIfUumgtrwY/UGjOaOQUOrVH1am
|
||||
54wkUiVEF0Qd3AD8KCl/l/xT5+t6cOspZ9GIhwa2NBmRjN/wVGp+Yrb08Re3kxPCX9bs5i
|
||||
Le+kHN0vuFr7RDo+eUoiSPhWl6FUqx2W9NZqekmEgKn3oKrfbGaMH1VLkaKWlzQ4xJzP0i
|
||||
adQbIXGryLEYASydemZtsAAAADAQABAAACABUYzBYEhDAaHSj+dsmcdKll8/tPko4fGXqq
|
||||
k+gT4t4GVUdl+Q4kcIFAhQs5b4BoDava39FE8H4V4CaMxYMc6g6vy0nB+TuO/Wt/0OmTf+
|
||||
TxMsBdoV29kCgwLYWzZ1Zq9geQK6g6nzzu5ymXRa3ApDcKC3UTfUhHKHQC3AvtjvEk0NPX
|
||||
/EfNhwuph5aQsHNVbNnOb2MGznf9tuGjckVQUWiSLs47s+t5rykylJ8tb6cbIQk3a3G5nz
|
||||
gDFSE8Rfo6/Wk2YnDkRX9XjlKC3Q0QWzZX6hYQvs6baRT3G3jxg9SZhn8PqPc4S34VdJvA
|
||||
rl8AbcpeZuKi/3J/5F1cD9GwMNcl4gM87piF20/r9mMvC4zBAEgyF8WBi4OjSu0+ccsEsb
|
||||
GSpxKK04OPTB7p8mLJ8hQUiREg5OuPEEcAoDSuHgdliE7nDHzuImbpTcAZcWhkJaUdBWI6
|
||||
qcnGPARzxAOmuzkY8Gq0MtcWge5QxnLWJyrfy43M984Cvxql/maLUij4eTbMDDwV7Qx30V
|
||||
P2tJp5+hOnitRwB6cQIg5N7/cTQdJ6eiFYuw0v3IfHjYmaolY8F3u38Zv2PPk50CorPRDG
|
||||
esx0a9Elm2UKPb145MtHGZtLH2mayRnDjnxr25iLwgokI06tCLCNvbkYLA7wVpJn81eKmZ
|
||||
tQBtbfqBSiDiLjCrehAAABAQDh8vmgPR95Kx1DeBxzMSja7TStP5V58JcUvLP4dAN6zqtt
|
||||
rMuKDfJRSIVYGkU8vXlME5E6phWP7R5lYY7+kLDbeZrYQg1AxkK8y4fUYkCLBuEcCjzWDK
|
||||
oqZQNskk4urbCdBIP6AR7d/LMCHBb9rk2iOuUeos6JHRKbPGP1lvH3hLkbH9CA0F41sz86
|
||||
JFg6u/XaRQ2CyhS7y7SQ8dmaANGz9LGdIRqIoZ8Hfht8t1VRbM9fzSb3xoxUItbHpk9R9g
|
||||
GZsHSryi7AtRmHt0uBrWIv6RbIY0epCbjdCLvHflbkPgwBM7UndgkOSIwQ4SQF8Fs+e9/r
|
||||
hV05h0Y81vd1RZvOAAABAQD5EgW3SpmYzeMmiP7MKkfIlbZtwVmRu4anTzWxlk5pJ9GXnC
|
||||
QoInULCipWAOeJbuLIgRWLU4VzhOUbYLNKQPXECARfgoto2VXoXZZ2q2O4aXaCpeyU6nE8
|
||||
VKbp4nU1jEg5hWB3PRwZ8Pzs4A93/9mrpVzLmCT+LW9Rlnp6tTpqcUKGugg8vr64SSgqnV
|
||||
ZFyQgHgw+ZGOG9w714urS3U97WNTeHXAs0p2YBOu5XW3JQ3jkRo7YyZF3+TtBxbgfHRZfH
|
||||
O2mFcMBD3Sn4t+LAbgnLye3S2/WZf/gQwdVB7BgrVqguzQ2hGoOxNiwadkIDsxb6r/u3n6
|
||||
2lScpHFDS0WnpRAAABAQDxzkV52VX6wAWkQe/2KFH9wTG0XFANmZUnnTPR8wd+b9E7HIr0
|
||||
Mdd8iAHOhLRvTy8mih53GGBptXK7GdABMZtkqDErbXhuC8xbi9uRLEHiRe/oBfWr8vYIZY
|
||||
awiw3/EqxaTv0HBMicdr2S31Bs2/mjrVuJH0wAaI9ueQnZizzjgWuzeNZMWq1qk0akUUdm
|
||||
PDVd58yBkt8lKlkOG0LJAn6JEG9oH9XiTFShHzu1dQmoC2bKVHdxL8WCcYFVtmyoMRcLZq
|
||||
u6d4nyKha02cYZB5hM3VcizJI5HY/A+H3fBkRR0hXgkU5R89w+8x9VSJkNVx+JGC7ziK4a
|
||||
kUjfOmR5WBdrAAAAE3Rlc3RAY2xvdWRmbGFyZS5jb20BAgMEBQYH
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgOsuFqKdzp/nC3wQfKVJBdHa8axtGryKplPkDjdSXT4kAAAADAQABAAACAQDrQqd6jzuo+yH7MStYWXu/H3dzt/JPFtn2VQ2pp+BfL8Es4Jz+dBIxyixVIuxCtseWFjN+ZkSFOK5ua5hSsdyGayfWaXUcio/xPloOC01supidLgfmg7hKPX5ceVC1TufvMdeBxr1Kcxa3UWGTHWa+R6I4qzR6ruFXZO4hnF+Zth8hW8kbCsvS5bNtIQTWC/GBNdkfEQKEhDKs2+KWOCLi7YwP9wiLoox5hSdpMvUXE70bgYgWLLu54dHA5k8gUeBib5Wu1ftdp9IXT///MKsLrfPNa3/oDTqEV6k7PZVe+btJ1yRpHuoxcBvuSKmxzT50qT2CBTsWqnFljlxV5mj1GVFWFFcYPLuYAED8UzC+/6pgKAneeVlt0ptqI8tC7xJxOUn2wwjTr9mIRjThuZ5nQBIznaBoMaOsVtue4c1/ldX+it4RSG0KuvIWegjGYqAfOcaNnnmrvsW1MeesAoWYoU/XWzcwshf2jhGC2uI62Xgp6eQh9S6aC2vBj9QaM5o5BQ6tUfVqbnjCRSJUQXRB3cAPwoKX+X/FPn63pw6yln0YiHBrY0GZGM3/BUan5itvTxF7eTE8Jf1uzmIt76Qc3S+4WvtEOj55SiJI+FaXoVSrHZb01mp6SYSAqfegqt9sZowfVUuRopaXNDjEnM/SJp1BshcavIsRgBLJ16Zm2wAAAAAAAAAAAAAAAQAAAA10ZXN0VXNlckB0ZXN0AAAADAAAAAh0ZXN0VXNlcgAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEA0c6EklYvC9B041qEGWDNuot6G4tTVm9LCQC0vA+v2n25ru9CINV68IljmXBORXBwfG6PdLhg0SEabZUbsNX5WrIVbGovcghKS6GRsqI5+Quhm+o8eG042JE/hBoYdZ19TcMEyPOGzHsx0U/BSN9ZJWVCxqN51iI6qyhz9f6jlX2LQBFEvXlhxgF3owBEf8UCZt/UvbZdmeeyKNQElPmiVLIJEAPCueECp7a2mjCiP3zqjDvSeeGk4CelB/1qZZ4V2n7fvbHZjAB5JJs4KXs5o8KgvQnqgQMxiLFZ4PATt4+mxEzh4JymppbqJOo2rYwOA3TAIEWWtYRV/ZKJ0AyhhQAAAQ8AAAAHc3NoLXJzYQAAAQC2lL+6JYTGOdz1zNnck6onrFcVpO2onCVAKP8HdLoCeH0/upIugaCocPKuzoURYEfiHQotviNeprE/2CyAroJ5VBdqWftEeHn3FFvBCQ1gwRQ7oci4C5n72t0vjWWE6WBylS0RqpJjr6EQ8a1vuwIqAQrEJPp2yNLjRH2WD7eicBh5f43VKOMr73DtyTh4xoF0C2sNBROudt58npTaYqRHQgoI25V/aCmuYBgM3wdAGcoEZGoSerMfhID7GcWkvemq2hF8mQsspG3zgnyQXk+ahagmefzxutDnr3KdrZ637La0/XwABvBZ9L4l5RiEilVI1Shl96F2qbBW2YZ64pUQ test@cloudflare.com
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDrQqd6jzuo+yH7MStYWXu/H3dzt/JPFtn2VQ2pp+BfL8Es4Jz+dBIxyixVIuxCtseWFjN+ZkSFOK5ua5hSsdyGayfWaXUcio/xPloOC01supidLgfmg7hKPX5ceVC1TufvMdeBxr1Kcxa3UWGTHWa+R6I4qzR6ruFXZO4hnF+Zth8hW8kbCsvS5bNtIQTWC/GBNdkfEQKEhDKs2+KWOCLi7YwP9wiLoox5hSdpMvUXE70bgYgWLLu54dHA5k8gUeBib5Wu1ftdp9IXT///MKsLrfPNa3/oDTqEV6k7PZVe+btJ1yRpHuoxcBvuSKmxzT50qT2CBTsWqnFljlxV5mj1GVFWFFcYPLuYAED8UzC+/6pgKAneeVlt0ptqI8tC7xJxOUn2wwjTr9mIRjThuZ5nQBIznaBoMaOsVtue4c1/ldX+it4RSG0KuvIWegjGYqAfOcaNnnmrvsW1MeesAoWYoU/XWzcwshf2jhGC2uI62Xgp6eQh9S6aC2vBj9QaM5o5BQ6tUfVqbnjCRSJUQXRB3cAPwoKX+X/FPn63pw6yln0YiHBrY0GZGM3/BUan5itvTxF7eTE8Jf1uzmIt76Qc3S+4WvtEOj55SiJI+FaXoVSrHZb01mp6SYSAqfegqt9sZowfVUuRopaXNDjEnM/SJp1BshcavIsRgBLJ16Zm2w== test@cloudflare.com
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAQEAzBO7TXxbpk7sGQm/Wa29N/NFe5uuoEQGC5hxfihmcvVgeKeNKiSS
|
||||
snxzCE1Y6SmNMoE4aQs92wtcn48GmxRwZSXbCqLq2CJrHfe9B2k3aPkJZpQkFMshcJGo7p
|
||||
G0Vlo7dWAbYf99/YKddf290uLK7vxw9ty0pM1hXSXHNShv1b+bTQm/COMZ5jNsncjc1yBH
|
||||
KGkFVHee9Dh4Z0xLlHipIyyNXXzI0RFYuHSNJz9GD310XQLIIroptr7+/7g6+sPPGsNlI+
|
||||
95OScba1/PQ2b/qy+KyIwNIMSd9ziJy5xnO7Vo3LrqQrza1Pkn2i29PljUcbc/F0hhXNIq
|
||||
ITdNWwVqsQAAA8iKllTIipZUyAAAAAdzc2gtcnNhAAABAQDME7tNfFumTuwZCb9Zrb0380
|
||||
V7m66gRAYLmHF+KGZy9WB4p40qJJKyfHMITVjpKY0ygThpCz3bC1yfjwabFHBlJdsKourY
|
||||
Imsd970HaTdo+QlmlCQUyyFwkajukbRWWjt1YBth/339gp11/b3S4sru/HD23LSkzWFdJc
|
||||
c1KG/Vv5tNCb8I4xnmM2ydyNzXIEcoaQVUd570OHhnTEuUeKkjLI1dfMjREVi4dI0nP0YP
|
||||
fXRdAsgiuim2vv7/uDr6w88aw2Uj73k5JxtrX89DZv+rL4rIjA0gxJ33OInLnGc7tWjcuu
|
||||
pCvNrU+SfaLb0+WNRxtz8XSGFc0iohN01bBWqxAAAAAwEAAQAAAQAKEtNFEOVpQS4QUlXa
|
||||
tGPJtj1wy4+EI7d0rRK1GoNsG0amzgZ+1Q1UuCXpe//uinmIy64gKUjlXhs1WRcHYqvlok
|
||||
e8r6wN/Szybr8q9Xuht+FJ6fgZ+qjs6JPBKvoO5SdYNOVFIhpzABaLs3nCRiWkRFvDI8Pa
|
||||
+rRap7m8mwFiOJtmdiIZYFxzw6xXwTsGCrWPKgTv3FKGZzXnCB9i7jC2vwT1MDYbcnzEH4
|
||||
Ba4dxI8bp6WWEX0biRIXj3jCtLb5gisNTSxdZs254Syh75HEXunSh2YO+yVSWQtZj19ewW
|
||||
6Rb1Z3x5rVfXcgSkg7gZd9EpbckIIg6+MFSH3wdGW6atAAAAgQDFXiMuNd4ZYwdyhjlM5n
|
||||
nFqQDXGgnwyNdiIqAapoqTdF5aZwNnbTU0fCFaDMLCQAHgntcgCEsW9A4HzDzYhOABKElv
|
||||
j973vXWF165wFiZwuKSfroq/6JH6CiIcjiqpszbnqSOzy1hq913RWILS6e9yMjxRv8PUjm
|
||||
E+IkcnfcFUwAAAAIEA+jwI3ICe8PGEIezV2tvQFeQy2Z2wGslu1yvqfTYEztSmtygns3wn
|
||||
ZBb+cBXCnpqUCtznG7hZhq7I4m1I47BYznULwwFiBTVtBASG5wNP7zeVKTVZ4SKprze+Fe
|
||||
I/nUZDJ5Q26um7eDbhvZ/n95GY+fucMVHoSBfX1wE16XBfp88AAACBANDHcgC4qP2oyOw/
|
||||
+p9HineMQd/ppG3fePe07jyZXLHLf0rByFveFgRAQ1m77O7FtP3fFKy3Y9nNy18LGq35ZK
|
||||
Blsz2B23bO8NuffgAhchDG7KzKFXCo+AraIj5znp/znK5zIkaiiSOQaYywJ36EooYVpRtj
|
||||
ep5ap6bBFDZ2e+V/AAAAEW1pa2VAQzAyWTUwVEdKR0g4AQ==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDME7tNfFumTuwZCb9Zrb0380V7m66gRAYLmHF+KGZy9WB4p40qJJKyfHMITVjpKY0ygThpCz3bC1yfjwabFHBlJdsKourYImsd970HaTdo+QlmlCQUyyFwkajukbRWWjt1YBth/339gp11/b3S4sru/HD23LSkzWFdJcc1KG/Vv5tNCb8I4xnmM2ydyNzXIEcoaQVUd570OHhnTEuUeKkjLI1dfMjREVi4dI0nP0YPfXRdAsgiuim2vv7/uDr6w88aw2Uj73k5JxtrX89DZv+rL4rIjA0gxJ33OInLnGc7tWjcuupCvNrU+SfaLb0+WNRxtz8XSGFc0iohN01bBWqx mike@C02Y50TGJGH8
|
||||
@@ -45,13 +45,11 @@ func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
|
||||
// Otherwise the host header will be based on the origin URL
|
||||
h1.Host = header.Value
|
||||
case ":path":
|
||||
// Separate the path and the (optional) query from each other
|
||||
chunks := strings.SplitN(header.Value, "?", 2)
|
||||
|
||||
h1.URL.Path = chunks[0]
|
||||
if len(chunks) > 1 {
|
||||
h1.URL.RawQuery = chunks[1]
|
||||
}
|
||||
// We can't just set `URL.Path`, because there's no way to ask the library to *not* escape it,
|
||||
// causing https://github.com/cloudflare/cloudflared/issues/124.
|
||||
// The only way to bypass the URL escape seems to currently be `URL.Opaque`.
|
||||
// See https://github.com/golang/go/issues/5777
|
||||
h1.URL.Opaque = fmt.Sprintf("//%v%v", h1.URL.Host, header.Value)
|
||||
case "content-length":
|
||||
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package streamhandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
@@ -84,7 +85,7 @@ func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
|
||||
[]h2mux.Header{
|
||||
h2mux.Header{
|
||||
Name: ":path",
|
||||
Value: "/?query",
|
||||
Value: "/?query=mock%20value",
|
||||
},
|
||||
h2mux.Header{
|
||||
Name: "Mock header",
|
||||
@@ -98,7 +99,94 @@ func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
|
||||
"Mock header": []string{"Mock value"},
|
||||
}, request.Header)
|
||||
|
||||
assert.Equal(t, "http://example.com/?query", request.URL.String())
|
||||
assert.Equal(t, "http://example.com/?query=mock%20value", request.URL.String())
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(
|
||||
[]h2mux.Header{
|
||||
h2mux.Header{
|
||||
Name: ":path",
|
||||
Value: "/mock%20path",
|
||||
},
|
||||
h2mux.Header{
|
||||
Name: "Mock header",
|
||||
Value: "Mock value",
|
||||
},
|
||||
},
|
||||
request,
|
||||
)
|
||||
|
||||
assert.Equal(t, http.Header{
|
||||
"Mock header": []string{"Mock value"},
|
||||
}, request.Header)
|
||||
|
||||
assert.Equal(t, "http://example.com/mock%20path", request.URL.String())
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) {
|
||||
expectedPaths := []string{
|
||||
"",
|
||||
"/",
|
||||
"//",
|
||||
"/%20",
|
||||
"/ ",
|
||||
"/ a ",
|
||||
"/a%20b",
|
||||
"/foo/bar;param?query#frag",
|
||||
"/a␠b",
|
||||
"/a-umlaut-ä",
|
||||
"/a-umlaut-%C3%A4",
|
||||
"/a-umlaut-%c3%a4",
|
||||
"/a#b#c",
|
||||
"/a#b␠c",
|
||||
"/a#b%20c",
|
||||
"/a#b c",
|
||||
"/\\",
|
||||
"/a\\",
|
||||
"/a\\b",
|
||||
"/a,b.c.",
|
||||
"/.",
|
||||
"/a`",
|
||||
"/a[0]",
|
||||
"/?a[0]=5 &b[]=",
|
||||
"/?a=%22b%20%22",
|
||||
}
|
||||
|
||||
for index, expectedPath := range expectedPaths {
|
||||
requestURL := "https://example.com"
|
||||
expectedURL := fmt.Sprintf("https://example.com%v", expectedPath)
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(
|
||||
[]h2mux.Header{
|
||||
h2mux.Header{
|
||||
Name: ":path",
|
||||
Value: expectedPath,
|
||||
},
|
||||
h2mux.Header{
|
||||
Name: "Mock header",
|
||||
Value: "Mock value",
|
||||
},
|
||||
},
|
||||
request,
|
||||
)
|
||||
|
||||
assert.Equal(t, http.Header{
|
||||
"Mock header": []string{"Mock value"},
|
||||
}, request.Header)
|
||||
|
||||
assert.Equal(t, expectedURL, request.URL.String(), fmt.Sprintf("Failed URL index: %v", index))
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,11 +82,18 @@ func (s *StreamHandler) UseConfiguration(ctx context.Context, config *pogs.Clien
|
||||
|
||||
// UpdateConfig replaces current originmapper mapping with mappings from newConfig
|
||||
func (s *StreamHandler) UpdateConfig(newConfig []*pogs.ReverseProxyConfig) (failedConfigs []*pogs.FailedConfig) {
|
||||
// TODO: TUN-1968: Gracefully apply new config
|
||||
s.tunnelHostnameMapper.DeleteAll()
|
||||
for _, tunnelConfig := range newConfig {
|
||||
|
||||
// Delete old configs that aren't in the `newConfig`
|
||||
toRemove := s.tunnelHostnameMapper.ToRemove(newConfig)
|
||||
for _, hostnameToRemove := range toRemove {
|
||||
s.tunnelHostnameMapper.Delete(hostnameToRemove)
|
||||
}
|
||||
|
||||
// Add new configs that weren't in the old mapper
|
||||
toAdd := s.tunnelHostnameMapper.ToAdd(newConfig)
|
||||
for _, tunnelConfig := range toAdd {
|
||||
tunnelHostname := tunnelConfig.TunnelHostname
|
||||
originSerice, err := tunnelConfig.OriginConfigJSONHandler.OriginConfig.Service()
|
||||
originSerice, err := tunnelConfig.OriginConfig.Service()
|
||||
if err != nil {
|
||||
s.logger.WithField("tunnelHostname", tunnelHostname).WithError(err).Error("Invalid origin service config")
|
||||
failedConfigs = append(failedConfigs, &pogs.FailedConfig{
|
||||
|
||||
@@ -49,10 +49,8 @@ func TestServeRequest(t *testing.T) {
|
||||
reverseProxyConfigs := []*pogs.ReverseProxyConfig{
|
||||
{
|
||||
TunnelHostname: testTunnelHostname,
|
||||
OriginConfigJSONHandler: &pogs.OriginConfigJSONHandler{
|
||||
OriginConfig: &pogs.HTTPOriginConfig{
|
||||
URLString: httpServer.URL,
|
||||
},
|
||||
OriginConfig: &pogs.HTTPOriginConfig{
|
||||
URLString: httpServer.URL,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -99,10 +97,8 @@ func TestServeBadRequest(t *testing.T) {
|
||||
reverseProxyConfigs := []*pogs.ReverseProxyConfig{
|
||||
{
|
||||
TunnelHostname: testTunnelHostname,
|
||||
OriginConfigJSONHandler: &pogs.OriginConfigJSONHandler{
|
||||
OriginConfig: &pogs.HTTPOriginConfig{
|
||||
URLString: "",
|
||||
},
|
||||
OriginConfig: &pogs.HTTPOriginConfig{
|
||||
URLString: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
@@ -18,6 +17,8 @@ import (
|
||||
const (
|
||||
OriginCAPoolFlag = "origin-ca-pool"
|
||||
CaCertFlag = "cacert"
|
||||
|
||||
edgeTLSServerName = "cftunnel.com"
|
||||
)
|
||||
|
||||
// CertReloader can load and reload a TLS certificate from a particular filepath.
|
||||
@@ -126,7 +127,7 @@ func CreateTunnelConfig(c *cli.Context) (*tls.Config, error) {
|
||||
rootCAs = append(rootCAs, c.String(CaCertFlag))
|
||||
}
|
||||
|
||||
userConfig := &TLSParameters{RootCAs: rootCAs}
|
||||
userConfig := &TLSParameters{RootCAs: rootCAs, ServerName: edgeTLSServerName}
|
||||
tlsConfig, err := GetConfig(userConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -142,10 +143,6 @@ func CreateTunnelConfig(c *cli.Context) (*tls.Config, error) {
|
||||
rootCAPool.AddCert(cert)
|
||||
}
|
||||
tlsConfig.RootCAs = rootCAPool
|
||||
tlsConfig.ServerName = "cftunnel.com"
|
||||
} else if edgeAddrs := c.StringSlice("edge"); len(edgeAddrs) > 0 {
|
||||
// Set for development environments and for testing specific origintunneld instances
|
||||
tlsConfig.ServerName, _, _ = net.SplitHostPort(edgeAddrs[0])
|
||||
}
|
||||
|
||||
if tlsConfig.ServerName == "" && !tlsConfig.InsecureSkipVerify {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
|
||||
|
||||
package tlsconfig
|
||||
|
||||
import (
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/originservice"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
// TunnelHostnameMapper maps TunnelHostname to an OriginService
|
||||
@@ -38,12 +39,55 @@ func (om *TunnelHostnameMapper) Add(key h2mux.TunnelHostname, os originservice.O
|
||||
om.tunnelHostnameToOrigin[key] = os
|
||||
}
|
||||
|
||||
// DeleteAll mappings, and shutdown all OriginService
|
||||
func (om *TunnelHostnameMapper) DeleteAll() {
|
||||
// Delete a mapping, and shutdown its OriginService
|
||||
func (om *TunnelHostnameMapper) Delete(key h2mux.TunnelHostname) (keyFound bool) {
|
||||
om.Lock()
|
||||
defer om.Unlock()
|
||||
for key, os := range om.tunnelHostnameToOrigin {
|
||||
if os, ok := om.tunnelHostnameToOrigin[key]; ok {
|
||||
os.Shutdown()
|
||||
delete(om.tunnelHostnameToOrigin, key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ToRemove finds all keys that should be removed from the TunnelHostnameMapper.
|
||||
func (om *TunnelHostnameMapper) ToRemove(newConfigs []*pogs.ReverseProxyConfig) (toRemove []h2mux.TunnelHostname) {
|
||||
om.Lock()
|
||||
defer om.Unlock()
|
||||
|
||||
// Convert into a set, for O(1) lookups instead of O(n)
|
||||
newConfigSet := toSet(newConfigs)
|
||||
|
||||
// If a config in `om` isn't in `newConfigs`, it must be removed.
|
||||
for hostname := range om.tunnelHostnameToOrigin {
|
||||
if _, ok := newConfigSet[hostname]; !ok {
|
||||
toRemove = append(toRemove, hostname)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ToAdd filters the given configs, keeping those that should be added to the TunnelHostnameMapper.
|
||||
func (om *TunnelHostnameMapper) ToAdd(newConfigs []*pogs.ReverseProxyConfig) (toAdd []*pogs.ReverseProxyConfig) {
|
||||
om.Lock()
|
||||
defer om.Unlock()
|
||||
|
||||
// If a config in `newConfigs` isn't in `om`, it must be added.
|
||||
for _, config := range newConfigs {
|
||||
if _, ok := om.tunnelHostnameToOrigin[config.TunnelHostname]; !ok {
|
||||
toAdd = append(toAdd, config)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func toSet(configs []*pogs.ReverseProxyConfig) map[h2mux.TunnelHostname]*pogs.ReverseProxyConfig {
|
||||
m := make(map[h2mux.TunnelHostname]*pogs.ReverseProxyConfig)
|
||||
for _, config := range configs {
|
||||
m[config.TunnelHostname] = config
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/originservice"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -52,9 +55,6 @@ func TestTunnelHostnameMapperConcurrentAccess(t *testing.T) {
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, secondHTTPOS, os)
|
||||
})
|
||||
|
||||
thm.DeleteAll()
|
||||
assert.Empty(t, thm.tunnelHostnameToOrigin)
|
||||
}
|
||||
|
||||
func concurrentOps(t *testing.T, f func(i int)) {
|
||||
@@ -72,3 +72,141 @@ func concurrentOps(t *testing.T, f func(i int)) {
|
||||
func tunnelHostname(i int) h2mux.TunnelHostname {
|
||||
return h2mux.TunnelHostname(fmt.Sprintf("%d.cftunnel.com", i))
|
||||
}
|
||||
|
||||
func Test_toSet(t *testing.T) {
|
||||
|
||||
type args struct {
|
||||
configs []*pogs.ReverseProxyConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want map[h2mux.TunnelHostname]*pogs.ReverseProxyConfig
|
||||
}{
|
||||
{
|
||||
name: "empty slice should yield empty map",
|
||||
args: args{},
|
||||
want: map[h2mux.TunnelHostname]*pogs.ReverseProxyConfig{},
|
||||
},
|
||||
{
|
||||
name: "multiple elements",
|
||||
args: args{[]*pogs.ReverseProxyConfig{sampleConfig1(), sampleConfig2()}},
|
||||
want: map[h2mux.TunnelHostname]*pogs.ReverseProxyConfig{
|
||||
"mock.example.com": sampleConfig1(),
|
||||
"mock2.example.com": sampleConfig2(),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := toSet(tt.args.configs); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("toSet() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunnelHostnameMapper_ToAdd(t *testing.T) {
|
||||
type fields struct {
|
||||
tunnelHostnameToOrigin map[h2mux.TunnelHostname]originservice.OriginService
|
||||
}
|
||||
type args struct {
|
||||
newConfigs []*pogs.ReverseProxyConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantToAdd []*pogs.ReverseProxyConfig
|
||||
}{
|
||||
{
|
||||
name: "Mapper={}, NewConfig={}, toAdd={}",
|
||||
},
|
||||
{
|
||||
name: "Mapper={}, NewConfig={x}, toAdd={x}",
|
||||
args: args{newConfigs: []*pogs.ReverseProxyConfig{sampleConfig1()}},
|
||||
wantToAdd: []*pogs.ReverseProxyConfig{sampleConfig1()},
|
||||
},
|
||||
{
|
||||
name: "Mapper={x}, NewConfig={x,y}, toAdd={y}",
|
||||
args: args{newConfigs: []*pogs.ReverseProxyConfig{sampleConfig2()}},
|
||||
wantToAdd: []*pogs.ReverseProxyConfig{sampleConfig2()},
|
||||
fields: fields{tunnelHostnameToOrigin: map[h2mux.TunnelHostname]originservice.OriginService{
|
||||
h2mux.TunnelHostname(sampleConfig1().TunnelHostname): &originservice.HelloWorldService{},
|
||||
}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
om := &TunnelHostnameMapper{
|
||||
tunnelHostnameToOrigin: tt.fields.tunnelHostnameToOrigin,
|
||||
}
|
||||
if gotToAdd := om.ToAdd(tt.args.newConfigs); !reflect.DeepEqual(gotToAdd, tt.wantToAdd) {
|
||||
t.Errorf("TunnelHostnameMapper.ToAdd() = %v, want %v", gotToAdd, tt.wantToAdd)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunnelHostnameMapper_ToRemove(t *testing.T) {
|
||||
type fields struct {
|
||||
tunnelHostnameToOrigin map[h2mux.TunnelHostname]originservice.OriginService
|
||||
}
|
||||
type args struct {
|
||||
newConfigs []*pogs.ReverseProxyConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantToRemove []h2mux.TunnelHostname
|
||||
}{
|
||||
{
|
||||
name: "Mapper={}, NewConfig={}, toRemove={}",
|
||||
},
|
||||
{
|
||||
name: "Mapper={x}, NewConfig={}, toRemove={x}",
|
||||
wantToRemove: []h2mux.TunnelHostname{sampleConfig1().TunnelHostname},
|
||||
fields: fields{tunnelHostnameToOrigin: map[h2mux.TunnelHostname]originservice.OriginService{
|
||||
h2mux.TunnelHostname(sampleConfig1().TunnelHostname): &originservice.HelloWorldService{},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "Mapper={x}, NewConfig={x}, toRemove={}",
|
||||
args: args{newConfigs: []*pogs.ReverseProxyConfig{sampleConfig1()}},
|
||||
fields: fields{tunnelHostnameToOrigin: map[h2mux.TunnelHostname]originservice.OriginService{
|
||||
h2mux.TunnelHostname(sampleConfig1().TunnelHostname): &originservice.HelloWorldService{},
|
||||
}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
om := &TunnelHostnameMapper{
|
||||
tunnelHostnameToOrigin: tt.fields.tunnelHostnameToOrigin,
|
||||
}
|
||||
if gotToRemove := om.ToRemove(tt.args.newConfigs); !reflect.DeepEqual(gotToRemove, tt.wantToRemove) {
|
||||
t.Errorf("TunnelHostnameMapper.ToRemove() = %v, want %v", gotToRemove, tt.wantToRemove)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func sampleConfig1() *pogs.ReverseProxyConfig {
|
||||
return &pogs.ReverseProxyConfig{
|
||||
TunnelHostname: "mock.example.com",
|
||||
OriginConfig: &pogs.HelloWorldOriginConfig{},
|
||||
Retries: 18,
|
||||
ConnectionTimeout: 5 * time.Second,
|
||||
CompressionQuality: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func sampleConfig2() *pogs.ReverseProxyConfig {
|
||||
return &pogs.ReverseProxyConfig{
|
||||
TunnelHostname: "mock2.example.com",
|
||||
OriginConfig: &pogs.HelloWorldOriginConfig{},
|
||||
Retries: 18,
|
||||
ConnectionTimeout: 5 * time.Second,
|
||||
CompressionQuality: 3,
|
||||
}
|
||||
}
|
||||
|
||||
+103
-130
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -29,11 +28,43 @@ import (
|
||||
|
||||
// ClientConfig is a collection of FallibleConfig that determines how cloudflared should function
|
||||
type ClientConfig struct {
|
||||
Version Version `json:"version"`
|
||||
SupervisorConfig *SupervisorConfig `json:"supervisor_config"`
|
||||
EdgeConnectionConfig *EdgeConnectionConfig `json:"edge_connection_config"`
|
||||
DoHProxyConfigs []*DoHProxyConfig `json:"doh_proxy_configs"`
|
||||
ReverseProxyConfigs []*ReverseProxyConfig `json:"reverse_proxy_configs"`
|
||||
Version Version
|
||||
SupervisorConfig *SupervisorConfig
|
||||
EdgeConnectionConfig *EdgeConnectionConfig
|
||||
DoHProxyConfigs []*DoHProxyConfig `capnp:"dohProxyConfigs"`
|
||||
ReverseProxyConfigs []*ReverseProxyConfig
|
||||
}
|
||||
|
||||
func (c *ClientConfig) MarshalBytes() ([]byte, error) {
|
||||
msg, firstSeg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
capnpEntity, err := tunnelrpc.NewRootClientConfig(firstSeg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = MarshalClientConfig(capnpEntity, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg.Marshal()
|
||||
}
|
||||
|
||||
func UnmarshalClientConfigFromBytes(clientConfigBytes []byte) (*ClientConfig, error) {
|
||||
msg, err := capnp.Unmarshal(clientConfigBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
capnpClientConfig, err := tunnelrpc.ReadRootClientConfig(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pogsClientConfig, err := UnmarshalClientConfig(capnpClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pogsClientConfig, nil
|
||||
}
|
||||
|
||||
// Version type models the version of a ClientConfig
|
||||
@@ -52,16 +83,17 @@ func (v Version) String() string {
|
||||
}
|
||||
|
||||
// FallibleConfig is an interface implemented by configs that cloudflared might not be able to apply
|
||||
//go-sumtype:decl FallibleConfig
|
||||
type FallibleConfig interface {
|
||||
FailReason(err error) string
|
||||
jsonType() string
|
||||
isFallibleConfig()
|
||||
}
|
||||
|
||||
// SupervisorConfig specifies config of components managed by Supervisor other than ConnectionManager
|
||||
type SupervisorConfig struct {
|
||||
AutoUpdateFrequency time.Duration `json:"auto_update_frequency"`
|
||||
MetricsUpdateFrequency time.Duration `json:"metrics_update_frequency"`
|
||||
GracePeriod time.Duration `json:"grace_period"`
|
||||
AutoUpdateFrequency time.Duration
|
||||
MetricsUpdateFrequency time.Duration
|
||||
GracePeriod time.Duration
|
||||
}
|
||||
|
||||
// FailReason impelents FallibleConfig interface for SupervisorConfig
|
||||
@@ -69,23 +101,15 @@ func (sc *SupervisorConfig) FailReason(err error) string {
|
||||
return fmt.Sprintf("Cannot apply SupervisorConfig, err: %v", err)
|
||||
}
|
||||
|
||||
func (sc *SupervisorConfig) MarshalJSON() ([]byte, error) {
|
||||
marshaler := make(map[string]SupervisorConfig, 1)
|
||||
marshaler[sc.jsonType()] = *sc
|
||||
return json.Marshal(marshaler)
|
||||
}
|
||||
|
||||
func (sc *SupervisorConfig) jsonType() string {
|
||||
return "supervisor_config"
|
||||
}
|
||||
func (_ *SupervisorConfig) isFallibleConfig() {}
|
||||
|
||||
// EdgeConnectionConfig specifies what parameters and how may connections should ConnectionManager establish with edge
|
||||
type EdgeConnectionConfig struct {
|
||||
NumHAConnections uint8 `json:"num_ha_connections"`
|
||||
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
MaxFailedHeartbeats uint64 `json:"max_failed_heartbeats"`
|
||||
UserCredentialPath string `json:"user_credential_path"`
|
||||
NumHAConnections uint8
|
||||
HeartbeatInterval time.Duration
|
||||
Timeout time.Duration
|
||||
MaxFailedHeartbeats uint64
|
||||
UserCredentialPath string
|
||||
}
|
||||
|
||||
// FailReason impelents FallibleConfig interface for EdgeConnectionConfig
|
||||
@@ -93,21 +117,13 @@ func (cmc *EdgeConnectionConfig) FailReason(err error) string {
|
||||
return fmt.Sprintf("Cannot apply EdgeConnectionConfig, err: %v", err)
|
||||
}
|
||||
|
||||
func (cmc *EdgeConnectionConfig) MarshalJSON() ([]byte, error) {
|
||||
marshaler := make(map[string]EdgeConnectionConfig, 1)
|
||||
marshaler[cmc.jsonType()] = *cmc
|
||||
return json.Marshal(marshaler)
|
||||
}
|
||||
|
||||
func (cmc *EdgeConnectionConfig) jsonType() string {
|
||||
return "edge_connection_config"
|
||||
}
|
||||
func (_ *EdgeConnectionConfig) isFallibleConfig() {}
|
||||
|
||||
// DoHProxyConfig is configuration for DNS over HTTPS service
|
||||
type DoHProxyConfig struct {
|
||||
ListenHost string `json:"listen_host"`
|
||||
ListenPort uint16 `json:"listen_port"`
|
||||
Upstreams []string `json:"upstreams"`
|
||||
ListenHost string
|
||||
ListenPort uint16
|
||||
Upstreams []string
|
||||
}
|
||||
|
||||
// FailReason impelents FallibleConfig interface for DoHProxyConfig
|
||||
@@ -115,23 +131,15 @@ func (dpc *DoHProxyConfig) FailReason(err error) string {
|
||||
return fmt.Sprintf("Cannot apply DoHProxyConfig, err: %v", err)
|
||||
}
|
||||
|
||||
func (dpc *DoHProxyConfig) MarshalJSON() ([]byte, error) {
|
||||
marshaler := make(map[string]DoHProxyConfig, 1)
|
||||
marshaler[dpc.jsonType()] = *dpc
|
||||
return json.Marshal(marshaler)
|
||||
}
|
||||
|
||||
func (dpc *DoHProxyConfig) jsonType() string {
|
||||
return "doh_proxy_config"
|
||||
}
|
||||
func (_ *DoHProxyConfig) isFallibleConfig() {}
|
||||
|
||||
// ReverseProxyConfig how and for what hostnames can this cloudflared proxy
|
||||
type ReverseProxyConfig struct {
|
||||
TunnelHostname h2mux.TunnelHostname `json:"tunnel_hostname"`
|
||||
OriginConfigJSONHandler *OriginConfigJSONHandler `json:"origin_config"`
|
||||
Retries uint64 `json:"retries"`
|
||||
ConnectionTimeout time.Duration `json:"connection_timeout"`
|
||||
CompressionQuality uint64 `json:"compression_quality"`
|
||||
TunnelHostname h2mux.TunnelHostname
|
||||
OriginConfig OriginConfig
|
||||
Retries uint64
|
||||
ConnectionTimeout time.Duration
|
||||
CompressionQuality uint64
|
||||
}
|
||||
|
||||
func NewReverseProxyConfig(
|
||||
@@ -145,11 +153,11 @@ func NewReverseProxyConfig(
|
||||
return nil, fmt.Errorf("NewReverseProxyConfig: originConfigUnmarshaler was null")
|
||||
}
|
||||
return &ReverseProxyConfig{
|
||||
TunnelHostname: h2mux.TunnelHostname(tunnelHostname),
|
||||
OriginConfigJSONHandler: &OriginConfigJSONHandler{originConfig},
|
||||
Retries: retries,
|
||||
ConnectionTimeout: connectionTimeout,
|
||||
CompressionQuality: compressionQuality,
|
||||
TunnelHostname: h2mux.TunnelHostname(tunnelHostname),
|
||||
OriginConfig: originConfig,
|
||||
Retries: retries,
|
||||
ConnectionTimeout: connectionTimeout,
|
||||
CompressionQuality: compressionQuality,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -158,58 +166,29 @@ func (rpc *ReverseProxyConfig) FailReason(err error) string {
|
||||
return fmt.Sprintf("Cannot apply ReverseProxyConfig, err: %v", err)
|
||||
}
|
||||
|
||||
func (rpc *ReverseProxyConfig) MarshalJSON() ([]byte, error) {
|
||||
marshaler := make(map[string]ReverseProxyConfig, 1)
|
||||
marshaler[rpc.jsonType()] = *rpc
|
||||
return json.Marshal(marshaler)
|
||||
}
|
||||
|
||||
func (rpc *ReverseProxyConfig) jsonType() string {
|
||||
return "reverse_proxy_config"
|
||||
}
|
||||
func (_ *ReverseProxyConfig) isFallibleConfig() {}
|
||||
|
||||
//go-sumtype:decl OriginConfig
|
||||
type OriginConfig interface {
|
||||
// Service returns a OriginService used to proxy to the origin
|
||||
Service() (originservice.OriginService, error)
|
||||
// go-sumtype requires at least one unexported method, otherwise it will complain that interface is not sealed
|
||||
jsonType() string
|
||||
}
|
||||
|
||||
type originType int
|
||||
|
||||
const (
|
||||
httpType originType = iota
|
||||
wsType
|
||||
helloWorldType
|
||||
)
|
||||
|
||||
func (ot originType) String() string {
|
||||
switch ot {
|
||||
case httpType:
|
||||
return "Http"
|
||||
case wsType:
|
||||
return "WebSocket"
|
||||
case helloWorldType:
|
||||
return "HelloWorld"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
isOriginConfig()
|
||||
}
|
||||
|
||||
type HTTPOriginConfig struct {
|
||||
URLString string `capnp:"urlString" json:"url_string" mapstructure:"url_string"`
|
||||
TCPKeepAlive time.Duration `capnp:"tcpKeepAlive" json:"tcp_keep_alive" mapstructure:"tcp_keep_alive"`
|
||||
DialDualStack bool `json:"dial_dual_stack" mapstructure:"dial_dual_stack"`
|
||||
TLSHandshakeTimeout time.Duration `capnp:"tlsHandshakeTimeout" json:"tls_handshake_timeout" mapstructure:"tls_handshake_timeout"`
|
||||
TLSVerify bool `capnp:"tlsVerify" json:"tls_verify" mapstructure:"tls_verify"`
|
||||
OriginCAPool string `json:"origin_ca_pool" mapstructure:"origin_ca_pool"`
|
||||
OriginServerName string `json:"origin_server_name" mapstructure:"origin_server_name"`
|
||||
MaxIdleConnections uint64 `json:"max_idle_connections" mapstructure:"max_idle_connections"`
|
||||
IdleConnectionTimeout time.Duration `json:"idle_connection_timeout" mapstructure:"idle_connection_timeout"`
|
||||
ProxyConnectionTimeout time.Duration `json:"proxy_connection_timeout" mapstructure:"proxy_connection_timeout"`
|
||||
ExpectContinueTimeout time.Duration `json:"expect_continue_timeout" mapstructure:"expect_continue_timeout"`
|
||||
ChunkedEncoding bool `json:"chunked_encoding" mapstructure:"chunked_encoding"`
|
||||
URLString string `capnp:"urlString"`
|
||||
TCPKeepAlive time.Duration `capnp:"tcpKeepAlive"`
|
||||
DialDualStack bool
|
||||
TLSHandshakeTimeout time.Duration `capnp:"tlsHandshakeTimeout"`
|
||||
TLSVerify bool `capnp:"tlsVerify"`
|
||||
OriginCAPool string
|
||||
OriginServerName string
|
||||
MaxIdleConnections uint64
|
||||
IdleConnectionTimeout time.Duration
|
||||
ProxyConnectionTimeout time.Duration
|
||||
ExpectContinueTimeout time.Duration
|
||||
ChunkedEncoding bool
|
||||
}
|
||||
|
||||
func (hc *HTTPOriginConfig) Service() (originservice.OriginService, error) {
|
||||
@@ -248,15 +227,13 @@ func (hc *HTTPOriginConfig) Service() (originservice.OriginService, error) {
|
||||
return originservice.NewHTTPService(transport, url, hc.ChunkedEncoding), nil
|
||||
}
|
||||
|
||||
func (_ *HTTPOriginConfig) jsonType() string {
|
||||
return httpType.String()
|
||||
}
|
||||
func (*HTTPOriginConfig) isOriginConfig() {}
|
||||
|
||||
type WebSocketOriginConfig struct {
|
||||
URLString string `capnp:"urlString" json:"url_string" mapstructure:"url_string"`
|
||||
TLSVerify bool `capnp:"tlsVerify" json:"tls_verify" mapstructure:"tls_verify"`
|
||||
OriginCAPool string `json:"origin_ca_pool" mapstructure:"origin_ca_pool"`
|
||||
OriginServerName string `json:"origin_server_name" mapstructure:"origin_server_name"`
|
||||
URLString string `capnp:"urlString"`
|
||||
TLSVerify bool `capnp:"tlsVerify"`
|
||||
OriginCAPool string
|
||||
OriginServerName string
|
||||
}
|
||||
|
||||
func (wsc *WebSocketOriginConfig) Service() (originservice.OriginService, error) {
|
||||
@@ -277,13 +254,11 @@ func (wsc *WebSocketOriginConfig) Service() (originservice.OriginService, error)
|
||||
return originservice.NewWebSocketService(tlsConfig, url)
|
||||
}
|
||||
|
||||
func (_ *WebSocketOriginConfig) jsonType() string {
|
||||
return wsType.String()
|
||||
}
|
||||
func (*WebSocketOriginConfig) isOriginConfig() {}
|
||||
|
||||
type HelloWorldOriginConfig struct{}
|
||||
|
||||
func (_ *HelloWorldOriginConfig) Service() (originservice.OriginService, error) {
|
||||
func (*HelloWorldOriginConfig) Service() (originservice.OriginService, error) {
|
||||
helloCert, err := tlsconfig.GetHelloCertificateX509()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Cannot get Hello World server certificate")
|
||||
@@ -308,9 +283,7 @@ func (_ *HelloWorldOriginConfig) Service() (originservice.OriginService, error)
|
||||
return originservice.NewHelloWorldService(transport)
|
||||
}
|
||||
|
||||
func (_ *HelloWorldOriginConfig) jsonType() string {
|
||||
return helloWorldType.String()
|
||||
}
|
||||
func (*HelloWorldOriginConfig) isOriginConfig() {}
|
||||
|
||||
/*
|
||||
* Boilerplate to convert between these structs and the primitive structs
|
||||
@@ -519,9 +492,9 @@ func UnmarshalDoHProxyConfig(s tunnelrpc.DoHProxyConfig) (*DoHProxyConfig, error
|
||||
|
||||
func MarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig, p *ReverseProxyConfig) error {
|
||||
s.SetTunnelHostname(p.TunnelHostname.String())
|
||||
switch config := p.OriginConfigJSONHandler.OriginConfig.(type) {
|
||||
switch config := p.OriginConfig.(type) {
|
||||
case *HTTPOriginConfig:
|
||||
ss, err := s.Origin().NewHttp()
|
||||
ss, err := s.OriginConfig().NewHttp()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -529,7 +502,7 @@ func MarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig, p *ReverseProxyCo
|
||||
return err
|
||||
}
|
||||
case *WebSocketOriginConfig:
|
||||
ss, err := s.Origin().NewWebsocket()
|
||||
ss, err := s.OriginConfig().NewWebsocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -537,7 +510,7 @@ func MarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig, p *ReverseProxyCo
|
||||
return err
|
||||
}
|
||||
case *HelloWorldOriginConfig:
|
||||
ss, err := s.Origin().NewHelloWorld()
|
||||
ss, err := s.OriginConfig().NewHelloWorld()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -560,9 +533,9 @@ func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyC
|
||||
return nil, err
|
||||
}
|
||||
p.TunnelHostname = h2mux.TunnelHostname(tunnelHostname)
|
||||
switch s.Origin().Which() {
|
||||
case tunnelrpc.ReverseProxyConfig_origin_Which_http:
|
||||
ss, err := s.Origin().Http()
|
||||
switch s.OriginConfig().Which() {
|
||||
case tunnelrpc.ReverseProxyConfig_originConfig_Which_http:
|
||||
ss, err := s.OriginConfig().Http()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -570,9 +543,9 @@ func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyC
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.OriginConfigJSONHandler = &OriginConfigJSONHandler{config}
|
||||
case tunnelrpc.ReverseProxyConfig_origin_Which_websocket:
|
||||
ss, err := s.Origin().Websocket()
|
||||
p.OriginConfig = config
|
||||
case tunnelrpc.ReverseProxyConfig_originConfig_Which_websocket:
|
||||
ss, err := s.OriginConfig().Websocket()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -580,9 +553,9 @@ func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyC
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.OriginConfigJSONHandler = &OriginConfigJSONHandler{config}
|
||||
case tunnelrpc.ReverseProxyConfig_origin_Which_helloWorld:
|
||||
ss, err := s.Origin().HelloWorld()
|
||||
p.OriginConfig = config
|
||||
case tunnelrpc.ReverseProxyConfig_originConfig_Which_helloWorld:
|
||||
ss, err := s.OriginConfig().HelloWorld()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -590,7 +563,7 @@ func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyC
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.OriginConfigJSONHandler = &OriginConfigJSONHandler{config}
|
||||
p.OriginConfig = config
|
||||
}
|
||||
p.Retries = s.Retries()
|
||||
p.ConnectionTimeout = time.Duration(s.ConnectionTimeout())
|
||||
@@ -690,13 +663,13 @@ func (i ClientService_PogsImpl) UseConfiguration(p tunnelrpc.ClientService_useCo
|
||||
}
|
||||
|
||||
type UseConfigurationResult struct {
|
||||
Success bool `json:"success"`
|
||||
FailedConfigs []*FailedConfig `json:"failed_configs"`
|
||||
Success bool
|
||||
FailedConfigs []*FailedConfig
|
||||
}
|
||||
|
||||
type FailedConfig struct {
|
||||
Config FallibleConfig `json:"config"`
|
||||
Reason string `json:"reason"`
|
||||
Config FallibleConfig
|
||||
Reason string
|
||||
}
|
||||
|
||||
func MarshalFailedConfig(s tunnelrpc.FailedConfig, p *FailedConfig) error {
|
||||
|
||||
@@ -29,36 +29,8 @@ func TestVersion(t *testing.T) {
|
||||
assert.True(t, secondVersion.IsNewerOrEqual(secondVersion))
|
||||
}
|
||||
|
||||
func TestClientConfig(t *testing.T) {
|
||||
addDoHProxyConfigs := func(c *ClientConfig) {
|
||||
c.DoHProxyConfigs = []*DoHProxyConfig{
|
||||
sampleDoHProxyConfig(),
|
||||
}
|
||||
}
|
||||
addReverseProxyConfigs := func(c *ClientConfig) {
|
||||
c.ReverseProxyConfigs = []*ReverseProxyConfig{
|
||||
sampleReverseProxyConfig(),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleHTTPOriginConfig()}
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleHTTPOriginConfigUnixPath()}
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleWebSocketOriginConfig()}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
testCases := []*ClientConfig{
|
||||
sampleClientConfig(),
|
||||
sampleClientConfig(addDoHProxyConfigs),
|
||||
sampleClientConfig(addReverseProxyConfigs),
|
||||
sampleClientConfig(addDoHProxyConfigs, addReverseProxyConfigs),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
func TestClientConfigCapnp(t *testing.T) {
|
||||
for i, testCase := range ClientConfigTestCases() {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewClientConfig(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
@@ -76,6 +48,52 @@ func TestClientConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func ClientConfigTestCases() []*ClientConfig {
|
||||
|
||||
addDoHProxyConfigs := func(c *ClientConfig) {
|
||||
c.DoHProxyConfigs = []*DoHProxyConfig{
|
||||
sampleDoHProxyConfig(),
|
||||
}
|
||||
}
|
||||
addReverseProxyConfigs := func(c *ClientConfig) {
|
||||
c.ReverseProxyConfigs = []*ReverseProxyConfig{
|
||||
sampleReverseProxyConfig(),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfig = sampleHTTPOriginConfig()
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfig = sampleHTTPOriginConfigUnixPath()
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfig = sampleWebSocketOriginConfig()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
testCases := []*ClientConfig{
|
||||
sampleClientConfig(),
|
||||
sampleClientConfig(addDoHProxyConfigs),
|
||||
sampleClientConfig(addReverseProxyConfigs),
|
||||
sampleClientConfig(addDoHProxyConfigs, addReverseProxyConfigs),
|
||||
}
|
||||
|
||||
return testCases
|
||||
}
|
||||
|
||||
func TestClientConfig(t *testing.T) {
|
||||
for _, testCase := range ClientConfigTestCases() {
|
||||
b, err := testCase.MarshalBytes()
|
||||
assert.NoError(t, err)
|
||||
|
||||
clientConfig, err := UnmarshalClientConfigFromBytes(b)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, testCase, clientConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseConfigurationResult(t *testing.T) {
|
||||
testCases := []*UseConfigurationResult{
|
||||
&UseConfigurationResult{
|
||||
@@ -142,13 +160,13 @@ func TestReverseProxyConfig(t *testing.T) {
|
||||
testCases := []*ReverseProxyConfig{
|
||||
sampleReverseProxyConfig(),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleHTTPOriginConfig()}
|
||||
c.OriginConfig = sampleHTTPOriginConfig()
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleHTTPOriginConfigUnixPath()}
|
||||
c.OriginConfig = sampleHTTPOriginConfigUnixPath()
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleWebSocketOriginConfig()}
|
||||
c.OriginConfig = sampleWebSocketOriginConfig()
|
||||
}),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
@@ -298,11 +316,11 @@ func sampleDoHProxyConfig(overrides ...func(*DoHProxyConfig)) *DoHProxyConfig {
|
||||
// applies any number of overrides to it, and returns it.
|
||||
func sampleReverseProxyConfig(overrides ...func(*ReverseProxyConfig)) *ReverseProxyConfig {
|
||||
sample := &ReverseProxyConfig{
|
||||
TunnelHostname: "mock-non-lb-tunnel.example.com",
|
||||
OriginConfigJSONHandler: &OriginConfigJSONHandler{&HelloWorldOriginConfig{}},
|
||||
Retries: 18,
|
||||
ConnectionTimeout: 5 * time.Second,
|
||||
CompressionQuality: 3,
|
||||
TunnelHostname: "mock-non-lb-tunnel.example.com",
|
||||
OriginConfig: &HelloWorldOriginConfig{},
|
||||
Retries: 18,
|
||||
ConnectionTimeout: 5 * time.Second,
|
||||
CompressionQuality: 3,
|
||||
}
|
||||
sample.ensureNoZeroFields()
|
||||
for _, f := range overrides {
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ScopeUnmarshaler can marshal a Scope pog from JSON.
|
||||
type ScopeUnmarshaler struct {
|
||||
Scope Scope
|
||||
}
|
||||
|
||||
// UnmarshalJSON takes in a JSON string, and attempts to marshal it into a Scope.
|
||||
// If successful, the Scope member of this ScopeUnmarshaler is set and nil is returned.
|
||||
// If unsuccessful, returns an error.
|
||||
func (su *ScopeUnmarshaler) UnmarshalJSON(b []byte) error {
|
||||
var scopeJSON map[string]interface{}
|
||||
if err := json.Unmarshal(b, &scopeJSON); err != nil {
|
||||
return errors.Wrapf(err, "cannot unmarshal %s into scopeJSON", string(b))
|
||||
}
|
||||
|
||||
if group, ok := scopeJSON["group"]; ok {
|
||||
if val, ok := group.(string); ok {
|
||||
su.Scope = NewGroup(val)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("JSON should have been a Scope, but the 'group' key contained %v", group)
|
||||
}
|
||||
|
||||
if systemName, ok := scopeJSON["system_name"]; ok {
|
||||
if val, ok := systemName.(string); ok {
|
||||
su.Scope = NewSystemName(val)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("JSON should have been a Scope, but the 'system_name' key contained %v", systemName)
|
||||
}
|
||||
|
||||
return fmt.Errorf("JSON should have been an object with one root key, either 'system_name' or 'group'")
|
||||
}
|
||||
|
||||
// OriginConfigJSONHandler is a wrapper to serialize OriginConfig with type information, and deserialize JSON
|
||||
// into an OriginConfig.
|
||||
type OriginConfigJSONHandler struct {
|
||||
OriginConfig OriginConfig
|
||||
}
|
||||
|
||||
func (ocjh *OriginConfigJSONHandler) MarshalJSON() ([]byte, error) {
|
||||
marshaler := make(map[string]OriginConfig, 1)
|
||||
marshaler[ocjh.OriginConfig.jsonType()] = ocjh.OriginConfig
|
||||
return json.Marshal(marshaler)
|
||||
}
|
||||
|
||||
func (ocjh *OriginConfigJSONHandler) UnmarshalJSON(b []byte) error {
|
||||
var originJSON map[string]interface{}
|
||||
if err := json.Unmarshal(b, &originJSON); err != nil {
|
||||
return errors.Wrapf(err, "cannot unmarshal %s into originJSON", string(b))
|
||||
}
|
||||
|
||||
if originConfig, ok := originJSON[httpType.String()]; ok {
|
||||
httpOriginConfig := &HTTPOriginConfig{}
|
||||
if err := mapstructure.Decode(originConfig, httpOriginConfig); err != nil {
|
||||
return errors.Wrapf(err, "cannot decode %+v into HTTPOriginConfig", originConfig)
|
||||
}
|
||||
ocjh.OriginConfig = httpOriginConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
if originConfig, ok := originJSON[wsType.String()]; ok {
|
||||
wsOriginConfig := &WebSocketOriginConfig{}
|
||||
if err := mapstructure.Decode(originConfig, wsOriginConfig); err != nil {
|
||||
return errors.Wrapf(err, "cannot decode %+v into WebSocketOriginConfig", originConfig)
|
||||
}
|
||||
ocjh.OriginConfig = wsOriginConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
if originConfig, ok := originJSON[helloWorldType.String()]; ok {
|
||||
helloWorldOriginConfig := &HelloWorldOriginConfig{}
|
||||
if err := mapstructure.Decode(originConfig, helloWorldOriginConfig); err != nil {
|
||||
return errors.Wrapf(err, "cannot decode %+v into HelloWorldOriginConfig", originConfig)
|
||||
}
|
||||
ocjh.OriginConfig = helloWorldOriginConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot unmarshal %s into OriginConfig", string(b))
|
||||
}
|
||||
|
||||
// FallibleConfigMarshaler is a wrapper for FallibleConfig to implement custom marshal logic
|
||||
type FallibleConfigMarshaler struct {
|
||||
FallibleConfig FallibleConfig
|
||||
}
|
||||
|
||||
func (fcm *FallibleConfigMarshaler) MarshalJSON() ([]byte, error) {
|
||||
marshaler := make(map[string]FallibleConfig, 1)
|
||||
marshaler[fcm.FallibleConfig.jsonType()] = fcm.FallibleConfig
|
||||
return json.Marshal(marshaler)
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScopeUnmarshaler_UnmarshalJSON(t *testing.T) {
|
||||
type fields struct {
|
||||
Scope Scope
|
||||
}
|
||||
type args struct {
|
||||
b []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
wantScope Scope
|
||||
}{
|
||||
{
|
||||
name: "group_successful",
|
||||
args: args{b: []byte(`{"group": "my-group"}`)},
|
||||
wantScope: NewGroup("my-group"),
|
||||
},
|
||||
{
|
||||
name: "system_name_successful",
|
||||
args: args{b: []byte(`{"system_name": "my-computer"}`)},
|
||||
wantScope: NewSystemName("my-computer"),
|
||||
},
|
||||
{
|
||||
name: "not_a_scope",
|
||||
args: args{b: []byte(`{"x": "y"}`)},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "malformed_group",
|
||||
args: args{b: []byte(`{"group": ["a", "b"]}`)},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
su := &ScopeUnmarshaler{
|
||||
Scope: tt.fields.Scope,
|
||||
}
|
||||
err := su.UnmarshalJSON(tt.args.b)
|
||||
if !tt.wantErr {
|
||||
if err != nil {
|
||||
t.Errorf("ScopeUnmarshaler.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !eqScope(tt.wantScope, su.Scope) {
|
||||
t.Errorf("Wanted scope %v but got scope %v", tt.wantScope, su.Scope)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalOrigin(t *testing.T) {
|
||||
tests := []struct {
|
||||
jsonLiteral string
|
||||
exceptedOriginConfig OriginConfig
|
||||
}{
|
||||
{
|
||||
jsonLiteral: `{
|
||||
"Http":{
|
||||
"url_string":"https.example.com",
|
||||
"tcp_keep_alive":7000000000,
|
||||
"dial_dual_stack":true,
|
||||
"tls_handshake_timeout":11000000000,
|
||||
"tls_verify":true,
|
||||
"origin_ca_pool":"/etc/cert.pem",
|
||||
"origin_server_name":"secure.example.com",
|
||||
"max_idle_connections":19,
|
||||
"idle_connection_timeout":17000000000,
|
||||
"proxy_connection_timeout":15000000000,
|
||||
"expect_continue_timeout":21000000000,
|
||||
"chunked_encoding":true
|
||||
}
|
||||
}`,
|
||||
exceptedOriginConfig: sampleHTTPOriginConfig(),
|
||||
},
|
||||
{
|
||||
jsonLiteral: `{
|
||||
"WebSocket":{
|
||||
"url_string":"ssh://example.com",
|
||||
"tls_verify":true,
|
||||
"origin_ca_pool":"/etc/cert.pem",
|
||||
"origin_server_name":"secure.example.com"
|
||||
}
|
||||
}`,
|
||||
exceptedOriginConfig: sampleWebSocketOriginConfig(),
|
||||
},
|
||||
{
|
||||
jsonLiteral: `{
|
||||
"HelloWorld": {}
|
||||
}`,
|
||||
exceptedOriginConfig: &HelloWorldOriginConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
originConfigJSON := prettyToValidJSON(test.jsonLiteral)
|
||||
var OriginConfigJSONHandler OriginConfigJSONHandler
|
||||
err := json.Unmarshal([]byte(originConfigJSON), &OriginConfigJSONHandler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.exceptedOriginConfig, OriginConfigJSONHandler.OriginConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalClientConfig(t *testing.T) {
|
||||
prettyClientConfigJSON := `{
|
||||
"version":10,
|
||||
"supervisor_config":{
|
||||
"auto_update_frequency":86400000000000,
|
||||
"metrics_update_frequency":300000000000,
|
||||
"grace_period":30000000000
|
||||
},
|
||||
"edge_connection_config":{
|
||||
"num_ha_connections":4,
|
||||
"heartbeat_interval":5000000000,
|
||||
"timeout":30000000000,
|
||||
"max_failed_heartbeats":5,
|
||||
"user_credential_path":"~/.cloudflared/cert.pem"
|
||||
},
|
||||
"doh_proxy_configs":[{
|
||||
"listen_host": "localhost",
|
||||
"listen_port": 53,
|
||||
"upstreams": ["https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"]
|
||||
}],
|
||||
"reverse_proxy_configs":[{
|
||||
"tunnel_hostname":"sdfjadk33.cftunnel.com",
|
||||
"origin_config":{
|
||||
"Http":{
|
||||
"url_string":"https://127.0.0.1:8080",
|
||||
"tcp_keep_alive":30000000000,
|
||||
"dial_dual_stack":true,
|
||||
"tls_handshake_timeout":10000000000,
|
||||
"tls_verify":true,
|
||||
"origin_ca_pool":"",
|
||||
"origin_server_name":"",
|
||||
"max_idle_connections":100,
|
||||
"idle_connection_timeout":90000000000,
|
||||
"proxy_connection_timeout":90000000000,
|
||||
"expect_continue_timeout":90000000000,
|
||||
"chunked_encoding":true
|
||||
}
|
||||
},
|
||||
"retries":5,
|
||||
"connection_timeout":30,
|
||||
"compression_quality":0
|
||||
}]
|
||||
}`
|
||||
// replace new line and tab
|
||||
clientConfigJSON := prettyToValidJSON(prettyClientConfigJSON)
|
||||
|
||||
var clientConfig ClientConfig
|
||||
err := json.Unmarshal([]byte(clientConfigJSON), &clientConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, Version(10), clientConfig.Version)
|
||||
|
||||
supervisorConfig := SupervisorConfig{
|
||||
AutoUpdateFrequency: time.Hour * 24,
|
||||
MetricsUpdateFrequency: time.Second * 300,
|
||||
GracePeriod: time.Second * 30,
|
||||
}
|
||||
assert.Equal(t, supervisorConfig, *clientConfig.SupervisorConfig)
|
||||
|
||||
edgeConnectionConfig := EdgeConnectionConfig{
|
||||
NumHAConnections: 4,
|
||||
HeartbeatInterval: time.Second * 5,
|
||||
Timeout: time.Second * 30,
|
||||
MaxFailedHeartbeats: 5,
|
||||
UserCredentialPath: "~/.cloudflared/cert.pem",
|
||||
}
|
||||
assert.Equal(t, edgeConnectionConfig, *clientConfig.EdgeConnectionConfig)
|
||||
|
||||
dohProxyConfig := DoHProxyConfig{
|
||||
ListenHost: "localhost",
|
||||
ListenPort: 53,
|
||||
Upstreams: []string{"https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"},
|
||||
}
|
||||
|
||||
assert.Len(t, clientConfig.DoHProxyConfigs, 1)
|
||||
assert.Equal(t, dohProxyConfig, *clientConfig.DoHProxyConfigs[0])
|
||||
|
||||
reverseProxyConfig := ReverseProxyConfig{
|
||||
TunnelHostname: "sdfjadk33.cftunnel.com",
|
||||
OriginConfigJSONHandler: &OriginConfigJSONHandler{
|
||||
OriginConfig: &HTTPOriginConfig{
|
||||
URLString: "https://127.0.0.1:8080",
|
||||
TCPKeepAlive: time.Second * 30,
|
||||
DialDualStack: true,
|
||||
TLSHandshakeTimeout: time.Second * 10,
|
||||
TLSVerify: true,
|
||||
OriginCAPool: "",
|
||||
OriginServerName: "",
|
||||
MaxIdleConnections: 100,
|
||||
IdleConnectionTimeout: time.Second * 90,
|
||||
ProxyConnectionTimeout: time.Second * 90,
|
||||
ExpectContinueTimeout: time.Second * 90,
|
||||
ChunkedEncoding: true,
|
||||
},
|
||||
},
|
||||
Retries: 5,
|
||||
ConnectionTimeout: 30,
|
||||
CompressionQuality: 0,
|
||||
}
|
||||
|
||||
assert.Len(t, clientConfig.ReverseProxyConfigs, 1)
|
||||
assert.Equal(t, reverseProxyConfig, *clientConfig.ReverseProxyConfigs[0])
|
||||
}
|
||||
|
||||
func TestMarshalFallibleConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
fallibleConfig FallibleConfig
|
||||
expctedJSONLiteral string
|
||||
}{
|
||||
{
|
||||
fallibleConfig: sampleSupervisorConfig(),
|
||||
expctedJSONLiteral: `{
|
||||
"supervisor_config":{
|
||||
"auto_update_frequency":75600000000000,
|
||||
"metrics_update_frequency":660000000000,
|
||||
"grace_period":31000000000
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
fallibleConfig: sampleEdgeConnectionConfig(),
|
||||
expctedJSONLiteral: `{
|
||||
"edge_connection_config":{
|
||||
"num_ha_connections":49,
|
||||
"heartbeat_interval":5000000000,
|
||||
"timeout":9000000000,
|
||||
"max_failed_heartbeats":9001,
|
||||
"user_credential_path":"/Users/example/.cloudflared/cert.pem"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
fallibleConfig: sampleDoHProxyConfig(),
|
||||
expctedJSONLiteral: `{
|
||||
"doh_proxy_config":{
|
||||
"listen_host":"127.0.0.1",
|
||||
"listen_port":53,
|
||||
"upstreams":["1.1.1.1","1.0.0.1"]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
fallibleConfig: sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleHTTPOriginConfig()}
|
||||
}),
|
||||
expctedJSONLiteral: `{
|
||||
"reverse_proxy_config":{
|
||||
"tunnel_hostname":"mock-non-lb-tunnel.example.com",
|
||||
"origin_config":{
|
||||
"Http":{
|
||||
"url_string":"https.example.com",
|
||||
"tcp_keep_alive":7000000000,
|
||||
"dial_dual_stack":true,
|
||||
"tls_handshake_timeout":11000000000,
|
||||
"tls_verify":true,
|
||||
"origin_ca_pool":"/etc/cert.pem",
|
||||
"origin_server_name":"secure.example.com",
|
||||
"max_idle_connections":19,
|
||||
"idle_connection_timeout":17000000000,
|
||||
"proxy_connection_timeout":15000000000,
|
||||
"expect_continue_timeout":21000000000,
|
||||
"chunked_encoding":true
|
||||
}
|
||||
},
|
||||
"retries":18,
|
||||
"connection_timeout":5000000000,
|
||||
"compression_quality":3
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
fallibleConfig: sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{sampleWebSocketOriginConfig()}
|
||||
}),
|
||||
expctedJSONLiteral: `{
|
||||
"reverse_proxy_config":{
|
||||
"tunnel_hostname":"mock-non-lb-tunnel.example.com",
|
||||
"origin_config":{
|
||||
"WebSocket":{
|
||||
"url_string":"ssh://example.com",
|
||||
"tls_verify":true,
|
||||
"origin_ca_pool":"/etc/cert.pem",
|
||||
"origin_server_name":"secure.example.com"
|
||||
}
|
||||
},
|
||||
"retries":18,
|
||||
"connection_timeout":5000000000,
|
||||
"compression_quality":3
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
fallibleConfig: sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.OriginConfigJSONHandler = &OriginConfigJSONHandler{&HelloWorldOriginConfig{}}
|
||||
}),
|
||||
expctedJSONLiteral: `{
|
||||
"reverse_proxy_config":{
|
||||
"tunnel_hostname":"mock-non-lb-tunnel.example.com",
|
||||
"origin_config":{
|
||||
"HelloWorld":{}
|
||||
},
|
||||
"retries":18,
|
||||
"connection_timeout":5000000000,
|
||||
"compression_quality":3
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
b, err := json.Marshal(test.fallibleConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, prettyToValidJSON(test.expctedJSONLiteral), string(b))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type prettyJSON string
|
||||
|
||||
func prettyToValidJSON(prettyJSON string) string {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(prettyJSON, "\n", ""), "\t", "")
|
||||
}
|
||||
|
||||
func eqScope(s1, s2 Scope) bool {
|
||||
return s1.Value() == s2.Value() && s1.PostgresType() == s2.PostgresType()
|
||||
}
|
||||
+121
-99
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
@@ -74,27 +75,129 @@ func UnmarshalRegistrationOptions(s tunnelrpc.RegistrationOptions) (*Registratio
|
||||
return p, err
|
||||
}
|
||||
|
||||
type ConnectResult struct {
|
||||
Err *ConnectError
|
||||
ServerInfo ServerInfo
|
||||
// ConnectResult models the result of Connect RPC, implemented by ConnectError and ConnectSuccess.
|
||||
type ConnectResult interface {
|
||||
ConnectError() *ConnectError
|
||||
ConnectedTo() string
|
||||
ClientConfig() *ClientConfig
|
||||
Marshal(s tunnelrpc.ConnectResult) error
|
||||
}
|
||||
|
||||
func MarshalConnectResult(s tunnelrpc.ConnectResult, p *ConnectResult) error {
|
||||
return pogs.Insert(tunnelrpc.ConnectResult_TypeID, s.Struct, p)
|
||||
func MarshalConnectResult(s tunnelrpc.ConnectResult, p ConnectResult) error {
|
||||
return p.Marshal(s)
|
||||
}
|
||||
|
||||
func UnmarshalConnectResult(s tunnelrpc.ConnectResult) (*ConnectResult, error) {
|
||||
p := new(ConnectResult)
|
||||
err := pogs.Extract(p, tunnelrpc.ConnectResult_TypeID, s.Struct)
|
||||
return p, err
|
||||
func UnmarshalConnectResult(s tunnelrpc.ConnectResult) (ConnectResult, error) {
|
||||
switch s.Result().Which() {
|
||||
case tunnelrpc.ConnectResult_result_Which_err:
|
||||
capnpConnectError, err := s.Result().Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return UnmarshalConnectError(capnpConnectError)
|
||||
case tunnelrpc.ConnectResult_result_Which_success:
|
||||
capnpConnectSuccess, err := s.Result().Success()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return UnmarshalConnectSuccess(capnpConnectSuccess)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unmarshal %v not implemented yet", s.Result().Which().String())
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectSuccess is the concrete returned type when Connect RPC succeed
|
||||
type ConnectSuccess struct {
|
||||
ServerLocationName string
|
||||
Config *ClientConfig
|
||||
}
|
||||
|
||||
func (*ConnectSuccess) ConnectError() *ConnectError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *ConnectSuccess) ConnectedTo() string {
|
||||
return cs.ServerLocationName
|
||||
}
|
||||
|
||||
func (cs *ConnectSuccess) ClientConfig() *ClientConfig {
|
||||
return cs.Config
|
||||
}
|
||||
|
||||
func (cs *ConnectSuccess) Marshal(s tunnelrpc.ConnectResult) error {
|
||||
capnpConnectSuccess, err := s.Result().NewSuccess()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = capnpConnectSuccess.SetServerLocationName(cs.ServerLocationName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set ConnectSuccess.ServerLocationName")
|
||||
}
|
||||
|
||||
if cs.Config != nil {
|
||||
capnpClientConfig, err := capnpConnectSuccess.NewClientConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to initialize ConnectSuccess.ClientConfig")
|
||||
}
|
||||
if err := MarshalClientConfig(capnpClientConfig, cs.Config); err != nil {
|
||||
return errors.Wrap(err, "failed to marshal ClientConfig")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnmarshalConnectSuccess(s tunnelrpc.ConnectSuccess) (*ConnectSuccess, error) {
|
||||
p := new(ConnectSuccess)
|
||||
|
||||
serverLocationName, err := s.ServerLocationName()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get tunnelrpc.ConnectSuccess.ServerLocationName")
|
||||
}
|
||||
p.ServerLocationName = serverLocationName
|
||||
|
||||
if s.HasClientConfig() {
|
||||
capnpClientConfig, err := s.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get tunnelrpc.ConnectSuccess.ClientConfig")
|
||||
}
|
||||
p.Config, err = UnmarshalClientConfig(capnpClientConfig)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get unmarshal ClientConfig")
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// ConnectError is the concrete returned type when Connect RPC encounters some error
|
||||
type ConnectError struct {
|
||||
Cause string
|
||||
RetryAfter time.Duration
|
||||
ShouldRetry bool
|
||||
}
|
||||
|
||||
func (ce *ConnectError) ConnectError() *ConnectError {
|
||||
return ce
|
||||
}
|
||||
|
||||
func (*ConnectError) ConnectedTo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (*ConnectError) ClientConfig() *ClientConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ce *ConnectError) Marshal(s tunnelrpc.ConnectResult) error {
|
||||
capnpConnectError, err := s.Result().NewErr()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalConnectError(capnpConnectError, ce)
|
||||
}
|
||||
|
||||
func MarshalConnectError(s tunnelrpc.ConnectError, p *ConnectError) error {
|
||||
return pogs.Insert(tunnelrpc.ConnectError_TypeID, s.Struct, p)
|
||||
}
|
||||
@@ -128,82 +231,13 @@ func UnmarshalServerInfo(s tunnelrpc.ServerInfo) (*ServerInfo, error) {
|
||||
return p, err
|
||||
}
|
||||
|
||||
//go-sumtype:decl Scope
|
||||
type Scope interface {
|
||||
Value() string
|
||||
PostgresType() string
|
||||
GraphQLType() string
|
||||
isScope()
|
||||
}
|
||||
|
||||
type SystemName struct {
|
||||
systemName string
|
||||
}
|
||||
|
||||
func NewSystemName(systemName string) *SystemName {
|
||||
return &SystemName{systemName: systemName}
|
||||
}
|
||||
|
||||
func (s *SystemName) Value() string { return s.systemName }
|
||||
func (_ *SystemName) PostgresType() string { return "system_name" }
|
||||
func (_ *SystemName) GraphQLType() string { return "SYSTEM_NAME" }
|
||||
|
||||
func (_ *SystemName) isScope() {}
|
||||
|
||||
type Group struct {
|
||||
group string
|
||||
}
|
||||
|
||||
func NewGroup(group string) *Group {
|
||||
return &Group{group: group}
|
||||
}
|
||||
|
||||
func (g *Group) Value() string { return g.group }
|
||||
func (_ *Group) PostgresType() string { return "group" }
|
||||
func (_ *Group) GraphQLType() string { return "GROUP" }
|
||||
|
||||
func (_ *Group) isScope() {}
|
||||
|
||||
func MarshalScope(s tunnelrpc.Scope, p Scope) error {
|
||||
ss := s.Value()
|
||||
switch scope := p.(type) {
|
||||
case *SystemName:
|
||||
ss.SetSystemName(scope.systemName)
|
||||
case *Group:
|
||||
ss.SetGroup(scope.group)
|
||||
default:
|
||||
return fmt.Errorf("unexpected Scope value: %v", p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnmarshalScope(s tunnelrpc.Scope) (Scope, error) {
|
||||
ss := s.Value()
|
||||
switch ss.Which() {
|
||||
case tunnelrpc.Scope_value_Which_systemName:
|
||||
systemName, err := ss.SystemName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewSystemName(systemName), nil
|
||||
case tunnelrpc.Scope_value_Which_group:
|
||||
group, err := ss.Group()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewGroup(group), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected Scope tag: %v", ss.Which())
|
||||
}
|
||||
}
|
||||
|
||||
type ConnectParameters struct {
|
||||
OriginCert []byte
|
||||
CloudflaredID uuid.UUID
|
||||
NumPreviousAttempts uint8
|
||||
Tags []Tag
|
||||
CloudflaredVersion string
|
||||
Scope Scope
|
||||
IntentLabel string
|
||||
}
|
||||
|
||||
func MarshalConnectParameters(s tunnelrpc.CapnpConnectParameters, p *ConnectParameters) error {
|
||||
@@ -236,11 +270,7 @@ func MarshalConnectParameters(s tunnelrpc.CapnpConnectParameters, p *ConnectPara
|
||||
if err := s.SetCloudflaredVersion(p.CloudflaredVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
scope, err := s.NewScope()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalScope(scope, p.Scope)
|
||||
return s.SetIntentLabel(p.IntentLabel)
|
||||
}
|
||||
|
||||
func UnmarshalConnectParameters(s tunnelrpc.CapnpConnectParameters) (*ConnectParameters, error) {
|
||||
@@ -281,22 +311,14 @@ func UnmarshalConnectParameters(s tunnelrpc.CapnpConnectParameters) (*ConnectPar
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scopeCapnp, err := s.Scope()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope, err := UnmarshalScope(scopeCapnp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
intentLabel, err := s.IntentLabel()
|
||||
return &ConnectParameters{
|
||||
OriginCert: originCert,
|
||||
CloudflaredID: cloudflaredID,
|
||||
NumPreviousAttempts: s.NumPreviousAttempts(),
|
||||
Tags: tags,
|
||||
CloudflaredVersion: cloudflaredVersion,
|
||||
Scope: scope,
|
||||
IntentLabel: intentLabel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -304,7 +326,7 @@ type TunnelServer interface {
|
||||
RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error)
|
||||
GetServerInfo(ctx context.Context) (*ServerInfo, error)
|
||||
UnregisterTunnel(ctx context.Context, gracePeriodNanoSec int64) error
|
||||
Connect(ctx context.Context, paramaters *ConnectParameters) (*ConnectResult, error)
|
||||
Connect(ctx context.Context, parameters *ConnectParameters) (ConnectResult, error)
|
||||
}
|
||||
|
||||
func TunnelServer_ServerToClient(s TunnelServer) tunnelrpc.TunnelServer {
|
||||
@@ -365,11 +387,11 @@ func (i TunnelServer_PogsImpl) UnregisterTunnel(p tunnelrpc.TunnelServer_unregis
|
||||
}
|
||||
|
||||
func (i TunnelServer_PogsImpl) Connect(p tunnelrpc.TunnelServer_connect) error {
|
||||
paramaters, err := p.Params.Parameters()
|
||||
parameters, err := p.Params.Parameters()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pogsParameters, err := UnmarshalConnectParameters(paramaters)
|
||||
pogsParameters, err := UnmarshalConnectParameters(parameters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -382,7 +404,7 @@ func (i TunnelServer_PogsImpl) Connect(p tunnelrpc.TunnelServer_connect) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalConnectResult(result, connectResult)
|
||||
return connectResult.Marshal(result)
|
||||
}
|
||||
|
||||
type TunnelServer_PogsClient struct {
|
||||
@@ -446,7 +468,7 @@ func (c TunnelServer_PogsClient) UnregisterTunnel(ctx context.Context, gracePeri
|
||||
|
||||
func (c TunnelServer_PogsClient) Connect(ctx context.Context,
|
||||
parameters *ConnectParameters,
|
||||
) (*ConnectResult, error) {
|
||||
) (ConnectResult, error) {
|
||||
client := tunnelrpc.TunnelServer{Client: c.Client}
|
||||
promise := client.Connect(ctx, func(p tunnelrpc.TunnelServer_connect_Params) error {
|
||||
connectParameters, err := p.NewParameters()
|
||||
|
||||
@@ -3,6 +3,7 @@ package pogs
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
"github.com/google/uuid"
|
||||
@@ -10,29 +11,34 @@ import (
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
)
|
||||
|
||||
// Assert *SystemName implements Scope
|
||||
var _ Scope = (*SystemName)(nil)
|
||||
|
||||
// Assert *Group implements Scope
|
||||
var _ Scope = (*Group)(nil)
|
||||
|
||||
func TestScope(t *testing.T) {
|
||||
testCases := []Scope{
|
||||
&SystemName{systemName: "my_system"},
|
||||
&Group{group: "my_group"},
|
||||
func TestConnectResult(t *testing.T) {
|
||||
testCases := []ConnectResult{
|
||||
&ConnectError{
|
||||
Cause: "it broke",
|
||||
ShouldRetry: false,
|
||||
RetryAfter: 2 * time.Second,
|
||||
},
|
||||
&ConnectSuccess{
|
||||
ServerLocationName: "SFO",
|
||||
Config: sampleClientConfig(),
|
||||
},
|
||||
&ConnectSuccess{
|
||||
ServerLocationName: "",
|
||||
Config: nil,
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewScope(seg)
|
||||
capnpEntity, err := tunnelrpc.NewConnectResult(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalScope(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
err = MarshalConnectResult(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase #%v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalScope(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
result, err := UnmarshalConnectResult(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase #%v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
@@ -43,7 +49,7 @@ func TestConnectParameters(t *testing.T) {
|
||||
testCases := []*ConnectParameters{
|
||||
sampleConnectParameters(),
|
||||
sampleConnectParameters(func(c *ConnectParameters) {
|
||||
c.Scope = &SystemName{systemName: "my_system"}
|
||||
c.IntentLabel = "my_intent"
|
||||
}),
|
||||
sampleConnectParameters(func(c *ConnectParameters) {
|
||||
c.Tags = nil
|
||||
@@ -83,7 +89,7 @@ func sampleConnectParameters(overrides ...func(*ConnectParameters)) *ConnectPara
|
||||
},
|
||||
},
|
||||
CloudflaredVersion: "7.0",
|
||||
Scope: &Group{group: "my_group"},
|
||||
IntentLabel: "my_intent",
|
||||
}
|
||||
sample.ensureNoZeroFields()
|
||||
for _, f := range overrides {
|
||||
|
||||
+14
-15
@@ -57,23 +57,15 @@ struct CapnpConnectParameters {
|
||||
tags @3 :List(Tag);
|
||||
# release version of cloudflared
|
||||
cloudflaredVersion @4 :Text;
|
||||
# identifier for this cloudflared instance
|
||||
scope @5 :Scope;
|
||||
}
|
||||
|
||||
struct Scope {
|
||||
value :union {
|
||||
# Standalone instance
|
||||
systemName @0 :Text;
|
||||
# Associated with a group of identical cloudflared instances
|
||||
group @1 :Text;
|
||||
}
|
||||
# which intent this cloudflared instance should get its behaviour from
|
||||
intentLabel @5 :Text;
|
||||
}
|
||||
|
||||
struct ConnectResult {
|
||||
err @0 :ConnectError;
|
||||
# Information about the server this connection is established with
|
||||
serverInfo @1 :ServerInfo;
|
||||
result :union {
|
||||
err @0 :ConnectError;
|
||||
success @1 :ConnectSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
struct ConnectError {
|
||||
@@ -83,6 +75,13 @@ struct ConnectError {
|
||||
shouldRetry @2 :Bool;
|
||||
}
|
||||
|
||||
struct ConnectSuccess {
|
||||
# Information about the server this connection is established with
|
||||
serverLocationName @0 :Text;
|
||||
# How this cloudflared instance should be configured. This can be null if there isn't an intent for this origin yet
|
||||
clientConfig @1 :ClientConfig;
|
||||
}
|
||||
|
||||
struct ClientConfig {
|
||||
# Version of this configuration. This value is opaque, but is guaranteed
|
||||
# to monotonically increase in value. Any configuration supplied to
|
||||
@@ -134,7 +133,7 @@ struct EdgeConnectionConfig {
|
||||
|
||||
struct ReverseProxyConfig {
|
||||
tunnelHostname @0 :Text;
|
||||
origin :union {
|
||||
originConfig :union {
|
||||
http @1 :HTTPOriginConfig;
|
||||
websocket @2 :WebSocketOriginConfig;
|
||||
helloWorld @3 :HelloWorldOriginConfig;
|
||||
|
||||
+436
-430
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
@@ -10,21 +9,15 @@ import (
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultScheme = "http"
|
||||
accessDomain = "cloudflareaccess.com"
|
||||
accessCertPath = "/cdn-cgi/access/certs"
|
||||
accessJwtHeader = "Cf-access-jwt-assertion"
|
||||
)
|
||||
const defaultScheme = "http"
|
||||
|
||||
var (
|
||||
supportedProtocols = []string{"http", "https", "rdp"}
|
||||
validationTimeout = time.Duration(30 * time.Second)
|
||||
validationTimeout = time.Duration(30 * time.Second)
|
||||
)
|
||||
|
||||
func ValidateHostname(hostname string) (string, error) {
|
||||
@@ -204,50 +197,3 @@ func toggleProtocol(httpProtocol string) string {
|
||||
return httpProtocol
|
||||
}
|
||||
}
|
||||
|
||||
// Access checks if a JWT from Cloudflare Access is valid.
|
||||
type Access struct {
|
||||
verifier *oidc.IDTokenVerifier
|
||||
}
|
||||
|
||||
func NewAccessValidator(ctx context.Context, domain, issuer, applicationAUD string) (*Access, error) {
|
||||
domainURL, err := ValidateUrl(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issuerURL, err := ValidateUrl(issuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// An issuerURL from Cloudflare Access will always use HTTPS.
|
||||
issuerURL = strings.Replace(issuerURL, "http:", "https:", 1)
|
||||
|
||||
keySet := oidc.NewRemoteKeySet(ctx, domainURL+accessCertPath)
|
||||
return &Access{oidc.NewVerifier(issuerURL, keySet, &oidc.Config{ClientID: applicationAUD})}, nil
|
||||
}
|
||||
|
||||
func (a *Access) Validate(ctx context.Context, jwt string) error {
|
||||
token, err := a.verifier.Verify(ctx, jwt)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "token is invalid: %s", jwt)
|
||||
}
|
||||
|
||||
// Perform extra sanity checks, just to be safe.
|
||||
|
||||
if token == nil {
|
||||
return fmt.Errorf("token is nil: %s", jwt)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(token.Issuer, accessDomain) {
|
||||
return fmt.Errorf("token has non-cloudflare issuer of %s: %s", token.Issuer, jwt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Access) ValidateRequest(ctx context.Context, r *http.Request) error {
|
||||
return a.Validate(ctx, r.Header.Get(accessJwtHeader))
|
||||
}
|
||||
|
||||
@@ -320,39 +320,6 @@ func TestValidateHTTPService_NonResponsiveOrigin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAccessValidatorOk(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
url := "test.cloudflareaccess.com"
|
||||
access, err := NewAccessValidator(ctx, url, url, "")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, access)
|
||||
|
||||
assert.Error(t, access.Validate(ctx, ""))
|
||||
assert.Error(t, access.Validate(ctx, "invalid"))
|
||||
|
||||
req := httptest.NewRequest("GET", "https://test.cloudflareaccess.com", nil)
|
||||
req.Header.Set(accessJwtHeader, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")
|
||||
assert.Error(t, access.ValidateRequest(ctx, req))
|
||||
}
|
||||
|
||||
func TestNewAccessValidatorErr(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
urls := []string{
|
||||
"",
|
||||
"tcp://test.cloudflareaccess.com",
|
||||
"wss://cloudflarenone.com",
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
access, err := NewAccessValidator(ctx, url, url, "")
|
||||
|
||||
assert.Error(t, err, url)
|
||||
assert.Nil(t, access)
|
||||
}
|
||||
}
|
||||
|
||||
type testRoundTripper func(req *http.Request) (*http.Response, error)
|
||||
|
||||
func (f testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
|
||||
-277
@@ -1,277 +0,0 @@
|
||||
// Copyright 2016 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package civil implements types for civil time, a time-zone-independent
|
||||
// representation of time that follows the rules of the proleptic
|
||||
// Gregorian calendar with exactly 24-hour days, 60-minute hours, and 60-second
|
||||
// minutes.
|
||||
//
|
||||
// Because they lack location information, these types do not represent unique
|
||||
// moments or intervals of time. Use time.Time for that purpose.
|
||||
package civil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Date represents a date (year, month, day).
|
||||
//
|
||||
// This type does not include location information, and therefore does not
|
||||
// describe a unique 24-hour timespan.
|
||||
type Date struct {
|
||||
Year int // Year (e.g., 2014).
|
||||
Month time.Month // Month of the year (January = 1, ...).
|
||||
Day int // Day of the month, starting at 1.
|
||||
}
|
||||
|
||||
// DateOf returns the Date in which a time occurs in that time's location.
|
||||
func DateOf(t time.Time) Date {
|
||||
var d Date
|
||||
d.Year, d.Month, d.Day = t.Date()
|
||||
return d
|
||||
}
|
||||
|
||||
// ParseDate parses a string in RFC3339 full-date format and returns the date value it represents.
|
||||
func ParseDate(s string) (Date, error) {
|
||||
t, err := time.Parse("2006-01-02", s)
|
||||
if err != nil {
|
||||
return Date{}, err
|
||||
}
|
||||
return DateOf(t), nil
|
||||
}
|
||||
|
||||
// String returns the date in RFC3339 full-date format.
|
||||
func (d Date) String() string {
|
||||
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
|
||||
}
|
||||
|
||||
// IsValid reports whether the date is valid.
|
||||
func (d Date) IsValid() bool {
|
||||
return DateOf(d.In(time.UTC)) == d
|
||||
}
|
||||
|
||||
// In returns the time corresponding to time 00:00:00 of the date in the location.
|
||||
//
|
||||
// In is always consistent with time.Date, even when time.Date returns a time
|
||||
// on a different day. For example, if loc is America/Indiana/Vincennes, then both
|
||||
// time.Date(1955, time.May, 1, 0, 0, 0, 0, loc)
|
||||
// and
|
||||
// civil.Date{Year: 1955, Month: time.May, Day: 1}.In(loc)
|
||||
// return 23:00:00 on April 30, 1955.
|
||||
//
|
||||
// In panics if loc is nil.
|
||||
func (d Date) In(loc *time.Location) time.Time {
|
||||
return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc)
|
||||
}
|
||||
|
||||
// AddDays returns the date that is n days in the future.
|
||||
// n can also be negative to go into the past.
|
||||
func (d Date) AddDays(n int) Date {
|
||||
return DateOf(d.In(time.UTC).AddDate(0, 0, n))
|
||||
}
|
||||
|
||||
// DaysSince returns the signed number of days between the date and s, not including the end day.
|
||||
// This is the inverse operation to AddDays.
|
||||
func (d Date) DaysSince(s Date) (days int) {
|
||||
// We convert to Unix time so we do not have to worry about leap seconds:
|
||||
// Unix time increases by exactly 86400 seconds per day.
|
||||
deltaUnix := d.In(time.UTC).Unix() - s.In(time.UTC).Unix()
|
||||
return int(deltaUnix / 86400)
|
||||
}
|
||||
|
||||
// Before reports whether d1 occurs before d2.
|
||||
func (d1 Date) Before(d2 Date) bool {
|
||||
if d1.Year != d2.Year {
|
||||
return d1.Year < d2.Year
|
||||
}
|
||||
if d1.Month != d2.Month {
|
||||
return d1.Month < d2.Month
|
||||
}
|
||||
return d1.Day < d2.Day
|
||||
}
|
||||
|
||||
// After reports whether d1 occurs after d2.
|
||||
func (d1 Date) After(d2 Date) bool {
|
||||
return d2.Before(d1)
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface.
|
||||
// The output is the result of d.String().
|
||||
func (d Date) MarshalText() ([]byte, error) {
|
||||
return []byte(d.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||
// The date is expected to be a string in a format accepted by ParseDate.
|
||||
func (d *Date) UnmarshalText(data []byte) error {
|
||||
var err error
|
||||
*d, err = ParseDate(string(data))
|
||||
return err
|
||||
}
|
||||
|
||||
// A Time represents a time with nanosecond precision.
|
||||
//
|
||||
// This type does not include location information, and therefore does not
|
||||
// describe a unique moment in time.
|
||||
//
|
||||
// This type exists to represent the TIME type in storage-based APIs like BigQuery.
|
||||
// Most operations on Times are unlikely to be meaningful. Prefer the DateTime type.
|
||||
type Time struct {
|
||||
Hour int // The hour of the day in 24-hour format; range [0-23]
|
||||
Minute int // The minute of the hour; range [0-59]
|
||||
Second int // The second of the minute; range [0-59]
|
||||
Nanosecond int // The nanosecond of the second; range [0-999999999]
|
||||
}
|
||||
|
||||
// TimeOf returns the Time representing the time of day in which a time occurs
|
||||
// in that time's location. It ignores the date.
|
||||
func TimeOf(t time.Time) Time {
|
||||
var tm Time
|
||||
tm.Hour, tm.Minute, tm.Second = t.Clock()
|
||||
tm.Nanosecond = t.Nanosecond()
|
||||
return tm
|
||||
}
|
||||
|
||||
// ParseTime parses a string and returns the time value it represents.
|
||||
// ParseTime accepts an extended form of the RFC3339 partial-time format. After
|
||||
// the HH:MM:SS part of the string, an optional fractional part may appear,
|
||||
// consisting of a decimal point followed by one to nine decimal digits.
|
||||
// (RFC3339 admits only one digit after the decimal point).
|
||||
func ParseTime(s string) (Time, error) {
|
||||
t, err := time.Parse("15:04:05.999999999", s)
|
||||
if err != nil {
|
||||
return Time{}, err
|
||||
}
|
||||
return TimeOf(t), nil
|
||||
}
|
||||
|
||||
// String returns the date in the format described in ParseTime. If Nanoseconds
|
||||
// is zero, no fractional part will be generated. Otherwise, the result will
|
||||
// end with a fractional part consisting of a decimal point and nine digits.
|
||||
func (t Time) String() string {
|
||||
s := fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second)
|
||||
if t.Nanosecond == 0 {
|
||||
return s
|
||||
}
|
||||
return s + fmt.Sprintf(".%09d", t.Nanosecond)
|
||||
}
|
||||
|
||||
// IsValid reports whether the time is valid.
|
||||
func (t Time) IsValid() bool {
|
||||
// Construct a non-zero time.
|
||||
tm := time.Date(2, 2, 2, t.Hour, t.Minute, t.Second, t.Nanosecond, time.UTC)
|
||||
return TimeOf(tm) == t
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface.
|
||||
// The output is the result of t.String().
|
||||
func (t Time) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||
// The time is expected to be a string in a format accepted by ParseTime.
|
||||
func (t *Time) UnmarshalText(data []byte) error {
|
||||
var err error
|
||||
*t, err = ParseTime(string(data))
|
||||
return err
|
||||
}
|
||||
|
||||
// A DateTime represents a date and time.
|
||||
//
|
||||
// This type does not include location information, and therefore does not
|
||||
// describe a unique moment in time.
|
||||
type DateTime struct {
|
||||
Date Date
|
||||
Time Time
|
||||
}
|
||||
|
||||
// Note: We deliberately do not embed Date into DateTime, to avoid promoting AddDays and Sub.
|
||||
|
||||
// DateTimeOf returns the DateTime in which a time occurs in that time's location.
|
||||
func DateTimeOf(t time.Time) DateTime {
|
||||
return DateTime{
|
||||
Date: DateOf(t),
|
||||
Time: TimeOf(t),
|
||||
}
|
||||
}
|
||||
|
||||
// ParseDateTime parses a string and returns the DateTime it represents.
|
||||
// ParseDateTime accepts a variant of the RFC3339 date-time format that omits
|
||||
// the time offset but includes an optional fractional time, as described in
|
||||
// ParseTime. Informally, the accepted format is
|
||||
// YYYY-MM-DDTHH:MM:SS[.FFFFFFFFF]
|
||||
// where the 'T' may be a lower-case 't'.
|
||||
func ParseDateTime(s string) (DateTime, error) {
|
||||
t, err := time.Parse("2006-01-02T15:04:05.999999999", s)
|
||||
if err != nil {
|
||||
t, err = time.Parse("2006-01-02t15:04:05.999999999", s)
|
||||
if err != nil {
|
||||
return DateTime{}, err
|
||||
}
|
||||
}
|
||||
return DateTimeOf(t), nil
|
||||
}
|
||||
|
||||
// String returns the date in the format described in ParseDate.
|
||||
func (dt DateTime) String() string {
|
||||
return dt.Date.String() + "T" + dt.Time.String()
|
||||
}
|
||||
|
||||
// IsValid reports whether the datetime is valid.
|
||||
func (dt DateTime) IsValid() bool {
|
||||
return dt.Date.IsValid() && dt.Time.IsValid()
|
||||
}
|
||||
|
||||
// In returns the time corresponding to the DateTime in the given location.
|
||||
//
|
||||
// If the time is missing or ambigous at the location, In returns the same
|
||||
// result as time.Date. For example, if loc is America/Indiana/Vincennes, then
|
||||
// both
|
||||
// time.Date(1955, time.May, 1, 0, 30, 0, 0, loc)
|
||||
// and
|
||||
// civil.DateTime{
|
||||
// civil.Date{Year: 1955, Month: time.May, Day: 1}},
|
||||
// civil.Time{Minute: 30}}.In(loc)
|
||||
// return 23:30:00 on April 30, 1955.
|
||||
//
|
||||
// In panics if loc is nil.
|
||||
func (dt DateTime) In(loc *time.Location) time.Time {
|
||||
return time.Date(dt.Date.Year, dt.Date.Month, dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc)
|
||||
}
|
||||
|
||||
// Before reports whether dt1 occurs before dt2.
|
||||
func (dt1 DateTime) Before(dt2 DateTime) bool {
|
||||
return dt1.In(time.UTC).Before(dt2.In(time.UTC))
|
||||
}
|
||||
|
||||
// After reports whether dt1 occurs after dt2.
|
||||
func (dt1 DateTime) After(dt2 DateTime) bool {
|
||||
return dt2.Before(dt1)
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface.
|
||||
// The output is the result of dt.String().
|
||||
func (dt DateTime) MarshalText() ([]byte, error) {
|
||||
return []byte(dt.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||
// The datetime is expected to be a string in a format accepted by ParseDateTime
|
||||
func (dt *DateTime) UnmarshalText(data []byte) error {
|
||||
var err error
|
||||
*dt, err = ParseDateTime(string(data))
|
||||
return err
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
/examples/blog/blog
|
||||
/examples/orders/orders
|
||||
/examples/basic/basic
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
language: go
|
||||
|
||||
go_import_path: github.com/DATA-DOG/go-sqlmock
|
||||
|
||||
go:
|
||||
- 1.2.x
|
||||
- 1.3.x
|
||||
- 1.4 # has no cover tool for latest releases
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
|
||||
sudo: false
|
||||
|
||||
script:
|
||||
- go vet
|
||||
- test -z "$(go fmt ./...)" # fail if not formatted properly
|
||||
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
The three clause BSD license (http://en.wikipedia.org/wiki/BSD_licenses)
|
||||
|
||||
Copyright (c) 2013-2019, DATA-DOG team
|
||||
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.
|
||||
|
||||
* The name DataDog.lt may not 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 MICHAEL BOSTOCK 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.
|
||||
-259
@@ -1,259 +0,0 @@
|
||||
[](https://travis-ci.org/DATA-DOG/go-sqlmock)
|
||||
[](https://godoc.org/github.com/DATA-DOG/go-sqlmock)
|
||||
[](https://goreportcard.com/report/github.com/DATA-DOG/go-sqlmock)
|
||||
[](https://codecov.io/github/DATA-DOG/go-sqlmock)
|
||||
|
||||
# Sql driver mock for Golang
|
||||
|
||||
**sqlmock** is a mock library implementing [sql/driver](https://godoc.org/database/sql/driver). Which has one and only
|
||||
purpose - to simulate any **sql** driver behavior in tests, without needing a real database connection. It helps to
|
||||
maintain correct **TDD** workflow.
|
||||
|
||||
- this library is now complete and stable. (you may not find new changes for this reason)
|
||||
- supports concurrency and multiple connections.
|
||||
- supports **go1.8** Context related feature mocking and Named sql parameters.
|
||||
- does not require any modifications to your source code.
|
||||
- the driver allows to mock any sql driver method behavior.
|
||||
- has strict by default expectation order matching.
|
||||
- has no third party dependencies.
|
||||
|
||||
**NOTE:** in **v1.2.0** **sqlmock.Rows** has changed to struct from interface, if you were using any type references to that
|
||||
interface, you will need to switch it to a pointer struct type. Also, **sqlmock.Rows** were used to implement **driver.Rows**
|
||||
interface, which was not required or useful for mocking and was removed. Hope it will not cause issues.
|
||||
|
||||
## Install
|
||||
|
||||
go get github.com/DATA-DOG/go-sqlmock
|
||||
|
||||
## Documentation and Examples
|
||||
|
||||
Visit [godoc](http://godoc.org/github.com/DATA-DOG/go-sqlmock) for general examples and public api reference.
|
||||
See **.travis.yml** for supported **go** versions.
|
||||
Different use case, is to functionally test with a real database - [go-txdb](https://github.com/DATA-DOG/go-txdb)
|
||||
all database related actions are isolated within a single transaction so the database can remain in the same state.
|
||||
|
||||
See implementation examples:
|
||||
|
||||
- [blog API server](https://github.com/DATA-DOG/go-sqlmock/tree/master/examples/blog)
|
||||
- [the same orders example](https://github.com/DATA-DOG/go-sqlmock/tree/master/examples/orders)
|
||||
|
||||
### Something you may want to test, assuming you use the [go-mysql-driver](https://github.com/go-sql-driver/mysql)
|
||||
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func recordStats(db *sql.DB, userID, productID int64) (err error) {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
switch err {
|
||||
case nil:
|
||||
err = tx.Commit()
|
||||
default:
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
// @NOTE: the real connection is not required for tests
|
||||
db, err := sql.Open("mysql", "root@/blog")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tests with sqlmock
|
||||
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
)
|
||||
|
||||
// a successful case
|
||||
func TestShouldUpdateStats(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
// now we execute our method
|
||||
if err = recordStats(db, 2, 3); err != nil {
|
||||
t.Errorf("error was not expected while updating stats: %s", err)
|
||||
}
|
||||
|
||||
// we make sure that all expectations were met
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("there were unfulfilled expectations: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// a failing test case
|
||||
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectExec("INSERT INTO product_viewers").
|
||||
WithArgs(2, 3).
|
||||
WillReturnError(fmt.Errorf("some error"))
|
||||
mock.ExpectRollback()
|
||||
|
||||
// now we execute our method
|
||||
if err = recordStats(db, 2, 3); err == nil {
|
||||
t.Errorf("was expecting an error, but there was none")
|
||||
}
|
||||
|
||||
// we make sure that all expectations were met
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("there were unfulfilled expectations: %s", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Customize SQL query matching
|
||||
|
||||
There were plenty of requests from users regarding SQL query string validation or different matching option.
|
||||
We have now implemented the `QueryMatcher` interface, which can be passed through an option when calling
|
||||
`sqlmock.New` or `sqlmock.NewWithDSN`.
|
||||
|
||||
This now allows to include some library, which would allow for example to parse and validate `mysql` SQL AST.
|
||||
And create a custom QueryMatcher in order to validate SQL in sophisticated ways.
|
||||
|
||||
By default, **sqlmock** is preserving backward compatibility and default query matcher is `sqlmock.QueryMatcherRegexp`
|
||||
which uses expected SQL string as a regular expression to match incoming query string. There is an equality matcher:
|
||||
`QueryMatcherEqual` which will do a full case sensitive match.
|
||||
|
||||
In order to customize the QueryMatcher, use the following:
|
||||
|
||||
``` go
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
```
|
||||
|
||||
The query matcher can be fully customized based on user needs. **sqlmock** will not
|
||||
provide a standard sql parsing matchers, since various drivers may not follow the same SQL standard.
|
||||
|
||||
## Matching arguments like time.Time
|
||||
|
||||
There may be arguments which are of `struct` type and cannot be compared easily by value like `time.Time`. In this case
|
||||
**sqlmock** provides an [Argument](https://godoc.org/github.com/DATA-DOG/go-sqlmock#Argument) interface which
|
||||
can be used in more sophisticated matching. Here is a simple example of time argument matching:
|
||||
|
||||
``` go
|
||||
type AnyTime struct{}
|
||||
|
||||
// Match satisfies sqlmock.Argument interface
|
||||
func (a AnyTime) Match(v driver.Value) bool {
|
||||
_, ok := v.(time.Time)
|
||||
return ok
|
||||
}
|
||||
|
||||
func TestAnyTimeArgument(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, mock, err := New()
|
||||
if err != nil {
|
||||
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
mock.ExpectExec("INSERT INTO users").
|
||||
WithArgs("john", AnyTime{}).
|
||||
WillReturnResult(NewResult(1, 1))
|
||||
|
||||
_, err = db.Exec("INSERT INTO users(name, created_at) VALUES (?, ?)", "john", time.Now())
|
||||
if err != nil {
|
||||
t.Errorf("error '%s' was not expected, while inserting a row", err)
|
||||
}
|
||||
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("there were unfulfilled expectations: %s", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It only asserts that argument is of `time.Time` type.
|
||||
|
||||
## Run tests
|
||||
|
||||
go test -race
|
||||
|
||||
## Change Log
|
||||
|
||||
- **2019-02-13** - added `go.mod` removed the references and suggestions using `gopkg.in`.
|
||||
- **2018-12-11** - added expectation of Rows to be closed, while mocking expected query.
|
||||
- **2018-12-11** - introduced an option to provide **QueryMatcher** in order to customize SQL query matching.
|
||||
- **2017-09-01** - it is now possible to expect that prepared statement will be closed,
|
||||
using **ExpectedPrepare.WillBeClosed**.
|
||||
- **2017-02-09** - implemented support for **go1.8** features. **Rows** interface was changed to struct
|
||||
but contains all methods as before and should maintain backwards compatibility. **ExpectedQuery.WillReturnRows** may now
|
||||
accept multiple row sets.
|
||||
- **2016-11-02** - `db.Prepare()` was not validating expected prepare SQL
|
||||
query. It should still be validated even if Exec or Query is not
|
||||
executed on that prepared statement.
|
||||
- **2016-02-23** - added **sqlmock.AnyArg()** function to provide any kind
|
||||
of argument matcher.
|
||||
- **2016-02-23** - convert expected arguments to driver.Value as natural
|
||||
driver does, the change may affect time.Time comparison and will be
|
||||
stricter. See [issue](https://github.com/DATA-DOG/go-sqlmock/issues/31).
|
||||
- **2015-08-27** - **v1** api change, concurrency support, all known issues fixed.
|
||||
- **2014-08-16** instead of **panic** during reflect type mismatch when comparing query arguments - now return error
|
||||
- **2014-08-14** added **sqlmock.NewErrorResult** which gives an option to return driver.Result with errors for
|
||||
interface methods, see [issue](https://github.com/DATA-DOG/go-sqlmock/issues/5)
|
||||
- **2014-05-29** allow to match arguments in more sophisticated ways, by providing an **sqlmock.Argument** interface
|
||||
- **2014-04-21** introduce **sqlmock.New()** to open a mock database connection for tests. This method
|
||||
calls sql.DB.Ping to ensure that connection is open, see [issue](https://github.com/DATA-DOG/go-sqlmock/issues/4).
|
||||
This way on Close it will surely assert if all expectations are met, even if database was not triggered at all.
|
||||
The old way is still available, but it is advisable to call db.Ping manually before asserting with db.Close.
|
||||
- **2014-02-14** RowsFromCSVString is now a part of Rows interface named as FromCSVString.
|
||||
It has changed to allow more ways to construct rows and to easily extend this API in future.
|
||||
See [issue 1](https://github.com/DATA-DOG/go-sqlmock/issues/1)
|
||||
**RowsFromCSVString** is deprecated and will be removed in future
|
||||
|
||||
## Contributions
|
||||
|
||||
Feel free to open a pull request. Note, if you wish to contribute an extension to public (exported methods or types) -
|
||||
please open an issue before, to discuss whether these changes can be accepted. All backward incompatible changes are
|
||||
and will be treated cautiously
|
||||
|
||||
## License
|
||||
|
||||
The [three clause BSD license](http://en.wikipedia.org/wiki/BSD_licenses)
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package sqlmock
|
||||
|
||||
import "database/sql/driver"
|
||||
|
||||
// Argument interface allows to match
|
||||
// any argument in specific way when used with
|
||||
// ExpectedQuery and ExpectedExec expectations.
|
||||
type Argument interface {
|
||||
Match(driver.Value) bool
|
||||
}
|
||||
|
||||
// AnyArg will return an Argument which can
|
||||
// match any kind of arguments.
|
||||
//
|
||||
// Useful for time.Time or similar kinds of arguments.
|
||||
func AnyArg() Argument {
|
||||
return anyArgument{}
|
||||
}
|
||||
|
||||
type anyArgument struct{}
|
||||
|
||||
func (a anyArgument) Match(_ driver.Value) bool {
|
||||
return true
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user