TUN-528: Move cloudflared into a separate repo

This commit is contained in:
Areg Harutyunyan
2018-05-01 18:45:06 -05:00
parent e8c621a648
commit d06fc520c7
4726 changed files with 1763680 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
// Copyright 2015 Light Code Labs, 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 websocket
import (
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("websocket", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new WebSocket middleware instance.
func setup(c *caddy.Controller) error {
websocks, err := webSocketParse(c)
if err != nil {
return err
}
GatewayInterface = caddy.AppName + "-CGI/1.1"
ServerSoftware = caddy.AppName + "/" + caddy.AppVersion
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return WebSocket{Next: next, Sockets: websocks}
})
return nil
}
func webSocketParse(c *caddy.Controller) ([]Config, error) {
var websocks []Config
var respawn bool
optionalBlock := func() (hadBlock bool, err error) {
for c.NextBlock() {
hadBlock = true
if c.Val() == "respawn" {
respawn = true
} else {
return true, c.Err("Expected websocket configuration parameter in block")
}
}
return
}
for c.Next() {
var val, path, command string
// Path or command; not sure which yet
if !c.NextArg() {
return nil, c.ArgErr()
}
val = c.Val()
// Extra configuration may be in a block
hadBlock, err := optionalBlock()
if err != nil {
return nil, err
}
if !hadBlock {
// The next argument on this line will be the command or an open curly brace
if c.NextArg() {
path = val
command = c.Val()
} else {
path = "/"
command = val
}
// Okay, check again for optional block
_, err = optionalBlock()
if err != nil {
return nil, err
}
}
// Split command into the actual command and its arguments
cmd, args, err := caddy.SplitCommandAndArgs(command)
if err != nil {
return nil, err
}
websocks = append(websocks, Config{
Path: path,
Command: cmd,
Arguments: args,
Respawn: respawn, // TODO: This isn't used currently
})
}
return websocks, nil
}

View File

@@ -0,0 +1,117 @@
// Copyright 2015 Light Code Labs, 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 websocket
import (
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestWebSocket(t *testing.T) {
c := caddy.NewTestController("http", `websocket cat`)
err := setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middleware, got 0 instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(WebSocket)
if !ok {
t.Fatalf("Expected handler to be type WebSocket, got: %#v", handler)
}
if myHandler.Sockets[0].Path != "/" {
t.Errorf("Expected / as the default Path")
}
if myHandler.Sockets[0].Command != "cat" {
t.Errorf("Expected %s as the command", "cat")
}
}
func TestWebSocketParse(t *testing.T) {
tests := []struct {
inputWebSocketConfig string
shouldErr bool
expectedWebSocketConfig []Config
}{
{`websocket /api1 cat`, false, []Config{{
Path: "/api1",
Command: "cat",
}}},
{`websocket /api3 cat
websocket /api4 cat `, false, []Config{{
Path: "/api3",
Command: "cat",
}, {
Path: "/api4",
Command: "cat",
}}},
{`websocket /api5 "cmd arg1 arg2 arg3"`, false, []Config{{
Path: "/api5",
Command: "cmd",
Arguments: []string{"arg1", "arg2", "arg3"},
}}},
// accept respawn
{`websocket /api6 cat {
respawn
}`, false, []Config{{
Path: "/api6",
Command: "cat",
}}},
// invalid configuration
{`websocket /api7 cat {
invalid
}`, true, []Config{}},
}
for i, test := range tests {
c := caddy.NewTestController("http", test.inputWebSocketConfig)
actualWebSocketConfigs, err := webSocketParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualWebSocketConfigs) != len(test.expectedWebSocketConfig) {
t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
i, len(test.expectedWebSocketConfig), len(actualWebSocketConfigs))
}
for j, actualWebSocketConfig := range actualWebSocketConfigs {
if actualWebSocketConfig.Path != test.expectedWebSocketConfig[j].Path {
t.Errorf("Test %d expected %dth WebSocket Config Path to be %s , but got %s",
i, j, test.expectedWebSocketConfig[j].Path, actualWebSocketConfig.Path)
}
if actualWebSocketConfig.Command != test.expectedWebSocketConfig[j].Command {
t.Errorf("Test %d expected %dth WebSocket Config Command to be %s , but got %s",
i, j, test.expectedWebSocketConfig[j].Command, actualWebSocketConfig.Command)
}
}
}
}

View File

@@ -0,0 +1,275 @@
// Copyright 2015 Light Code Labs, 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 websocket implements a WebSocket server by executing
// a command and piping its input and output through the WebSocket
// connection.
package websocket
import (
"bufio"
"bytes"
"io"
"net"
"net/http"
"os"
"os/exec"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 1024 * 1024 * 10 // 10 MB default.
)
var (
// GatewayInterface is the dialect of CGI being used by the server
// to communicate with the script. See CGI spec, 4.1.4
GatewayInterface string
// ServerSoftware is the name and version of the information server
// software making the CGI request. See CGI spec, 4.1.17
ServerSoftware string
)
type (
// WebSocket is a type that holds configuration for the
// websocket middleware generally, like a list of all the
// websocket endpoints.
WebSocket struct {
// Next is the next HTTP handler in the chain for when the path doesn't match
Next httpserver.Handler
// Sockets holds all the web socket endpoint configurations
Sockets []Config
}
// Config holds the configuration for a single websocket
// endpoint which may serve multiple websocket connections.
Config struct {
Path string
Command string
Arguments []string
Respawn bool // TODO: Not used, but parser supports it until we decide on it
}
)
// ServeHTTP converts the HTTP request to a WebSocket connection and serves it up.
func (ws WebSocket) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for _, sockconfig := range ws.Sockets {
if httpserver.Path(r.URL.Path).Matches(sockconfig.Path) {
return serveWS(w, r, &sockconfig)
}
}
// Didn't match a websocket path, so pass-through
return ws.Next.ServeHTTP(w, r)
}
// serveWS is used for setting and upgrading the HTTP connection to a websocket connection.
// It also spawns the child process that is associated with matched HTTP path/url.
func serveWS(w http.ResponseWriter, r *http.Request, config *Config) (int, error) {
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
// the connection has been "handled" -- WriteHeader was called with Upgrade,
// so don't return an error status code; just return an error
return 0, err
}
defer conn.Close()
cmd := exec.Command(config.Command, config.Arguments...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return http.StatusBadGateway, err
}
defer stdout.Close()
stdin, err := cmd.StdinPipe()
if err != nil {
return http.StatusBadGateway, err
}
defer stdin.Close()
metavars, err := buildEnv(cmd.Path, r)
if err != nil {
return http.StatusBadGateway, err
}
cmd.Env = metavars
if err := cmd.Start(); err != nil {
return http.StatusBadGateway, err
}
done := make(chan struct{})
go pumpStdout(conn, stdout, done)
pumpStdin(conn, stdin)
stdin.Close() // close stdin to end the process
if err := cmd.Process.Signal(os.Interrupt); err != nil { // signal an interrupt to kill the process
return http.StatusInternalServerError, err
}
select {
case <-done:
case <-time.After(time.Second):
// terminate with extreme prejudice.
if err := cmd.Process.Signal(os.Kill); err != nil {
return http.StatusInternalServerError, err
}
<-done
}
// not sure what we want to do here.
// status for an "exited" process is greater
// than 0, but isn't really an error per se.
// just going to ignore it for now.
cmd.Wait()
return 0, nil
}
// buildEnv creates the meta-variables for the child process according
// to the CGI 1.1 specification: http://tools.ietf.org/html/rfc3875#section-4.1
// cmdPath should be the path of the command being run.
// The returned string slice can be set to the command's Env property.
func buildEnv(cmdPath string, r *http.Request) (metavars []string, err error) {
if !strings.Contains(r.RemoteAddr, ":") {
r.RemoteAddr += ":"
}
remoteHost, remotePort, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return
}
if !strings.Contains(r.Host, ":") {
r.Host += ":"
}
serverHost, serverPort, err := net.SplitHostPort(r.Host)
if err != nil {
return
}
metavars = []string{
`AUTH_TYPE=`, // Not used
`CONTENT_LENGTH=`, // Not used
`CONTENT_TYPE=`, // Not used
`GATEWAY_INTERFACE=` + GatewayInterface,
`PATH_INFO=`, // TODO
`PATH_TRANSLATED=`, // TODO
`QUERY_STRING=` + r.URL.RawQuery,
`REMOTE_ADDR=` + remoteHost,
`REMOTE_HOST=` + remoteHost, // Host lookups are slow - don't do them
`REMOTE_IDENT=`, // Not used
`REMOTE_PORT=` + remotePort,
`REMOTE_USER=`, // Not used,
`REQUEST_METHOD=` + r.Method,
`REQUEST_URI=` + r.RequestURI,
`SCRIPT_NAME=` + cmdPath, // path of the program being executed
`SERVER_NAME=` + serverHost,
`SERVER_PORT=` + serverPort,
`SERVER_PROTOCOL=` + r.Proto,
`SERVER_SOFTWARE=` + ServerSoftware,
}
// Add each HTTP header to the environment as well
for header, values := range r.Header {
value := strings.Join(values, ", ")
header = strings.ToUpper(header)
header = strings.Replace(header, "-", "_", -1)
value = strings.Replace(value, "\n", " ", -1)
metavars = append(metavars, "HTTP_"+header+"="+value)
}
return
}
// pumpStdin handles reading data from the websocket connection and writing
// it to stdin of the process.
func pumpStdin(conn *websocket.Conn, stdin io.WriteCloser) {
// Setup our connection's websocket ping/pong handlers from our const values.
defer conn.Close()
conn.SetReadLimit(maxMessageSize)
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := conn.ReadMessage()
if err != nil {
break
}
message = append(message, '\n')
if _, err := stdin.Write(message); err != nil {
break
}
}
}
// pumpStdout handles reading data from stdout of the process and writing
// it to websocket connection.
func pumpStdout(conn *websocket.Conn, stdout io.Reader, done chan struct{}) {
go pinger(conn, done)
defer func() {
conn.Close()
close(done) // make sure to close the pinger when we are done.
}()
s := bufio.NewScanner(stdout)
for s.Scan() {
conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := conn.WriteMessage(websocket.TextMessage, bytes.TrimSpace(s.Bytes())); err != nil {
break
}
}
if s.Err() != nil {
conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, s.Err().Error()), time.Time{})
}
}
// pinger simulates the websocket to keep it alive with ping messages.
func pinger(conn *websocket.Conn, done chan struct{}) {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for { // blocking loop with select to wait for stimulation.
select {
case <-ticker.C:
if err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil {
conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, err.Error()), time.Time{})
return
}
case <-done:
return // clean up this routine.
}
}
}

View File

@@ -0,0 +1,36 @@
// Copyright 2015 Light Code Labs, 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 websocket
import (
"net/http"
"testing"
)
func TestBuildEnv(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost", nil)
if err != nil {
t.Fatal("Error setting up request:", err)
}
req.RemoteAddr = "localhost:50302"
env, err := buildEnv("/bin/command", req)
if err != nil {
t.Fatal("Didn't expect an error:", err)
}
if len(env) == 0 {
t.Fatalf("Expected non-empty environment; got %#v", env)
}
}