commands

package
v0.0.0-...-5da1e5c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2021 License: Apache-2.0 Imports: 39 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var CopyToClipboardFlag = &cli.BoolFlag{
	Name:    "copy",
	Aliases: []string{"c"},
	EnvVars: []string{"TUNL_COPY_ADDRESS"},
	Usage:   "Copies the public address to the clipboard",
}
View Source
var DaemonCommand = &cli.Command{
	Name:   "daemon",
	Hidden: true,
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "bind",
			Value: ":8080",
		},
		&cli.StringSliceFlag{
			Name: "tls-certs",
		},
		&cli.StringFlag{
			Name:  "control",
			Value: "_.tunl.es",
		},
		&cli.StringFlag{
			Name:  "domain",
			Value: "tunl.es",
		},
		&cli.StringFlag{
			Name:  "address-template",
			Value: "https://{{.Id}}.{{.Domain}}",
		},
		&cli.StringFlag{
			Name:  "sign-key",
			Value: xid.New().String(),
		},
		&cli.StringFlag{
			Name: "metrics.honeycomb.token",
		},
		&cli.StringFlag{
			Name:  "metrics.honeycomb.name",
			Value: "tunl",
		},
	},
	Action: func(ctx *cli.Context) error {
		signKey := ctx.String("sign-key")
		if len(signKey) == 0 {
			logger.Error("sign-key cannot be empty")
			return nil
		}

		bind := ctx.String("bind")
		if len(bind) == 0 {
			logger.Error("bind flag value cannot be empty")
			return nil
		}

		if token := ctx.String("metrics.honeycomb.token"); len(token) > 0 {
			dataset := "tunl"
			if value := ctx.String("metrics.honeycomb.dataset"); len(value) > 0 {
				dataset = value
			}

			go honeycomb.Honeycomb(metrics.DefaultRegistry, 10*time.Second, token, dataset, []float64{0.50, 0.75, 0.95, 0.99})
			logger.Info("honeycomb sink configurated", zap.String("dataset", dataset))
		}

		tunnelCount := metrics.GetOrRegisterCounter("tunnel", nil)
		connectionCount := metrics.GetOrRegisterCounter("connections", nil)

		var listener net.Listener
		if certGlobs := ctx.StringSlice("tls-certs"); len(certGlobs) > 0 {
			certs, err := certs.LoadCertificates(certGlobs)
			if err != nil {
				logger.Error("load certificate error", zap.Error(err), zap.Strings("certs", certGlobs))
				return nil
			}

			tlsListener, err := tls.Listen("tcp", bind, &tls.Config{
				Certificates: certs,
			})
			if err != nil {
				logger.Error("listen error failed to listen", zap.Error(err), zap.String("bind", bind))
				return nil
			}
			listener = tlsListener
		} else {
			nonTlsListener, err := net.Listen("tcp", bind)
			if err != nil {
				logger.Error("listen error failed to listen", zap.Error(err), zap.String("bind", bind))
				return nil
			}
			listener = nonTlsListener
		}

		logger.Debug("listener created", zap.String("address", listener.Addr().String()))

		mux, err := vhost.NewHTTPMuxer(listener, 30*time.Second)
		if err != nil {
			logger.Error("vhost mux creation error", zap.Error(err))
			return nil
		}
		defer mux.Close()

		addresses := server.NewAddresses(logger, ctx.String("domain"), mux)

		failed := make(chan error)

		go func() {
			logger.Debug("creating control vhost", zap.String("control", ctx.String("control")))

			control, err := mux.Listen(ctx.String("control"))
			if err != nil {
				failed <- errors.Wrap(err, "control vhost listener creation error")
				return
			}

			logger.Debug("control vhost created", zap.String("control", ctx.String("control")))

			failed <- http.Serve(control, http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
				if request.Method != http.MethodConnect {
					http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
					return
				}

				conn, _, err := response.(http.Hijacker).Hijack()
				if err != nil {
					logger.Debug("http hijack error", zap.Error(err))
					http.Error(response, "internal server error", http.StatusInternalServerError)
					return
				}
				defer conn.Close()

				tunlType := request.Header.Get("X-Tunl-Type")
				tunlToken := request.Header.Get("X-Tunl-Token")

				var address *server.PublicAddress

				if len(tunlToken) > 0 {
					token, err := verifyToken(signKey, tunlToken)
					if err != nil {
						logger.Info("invalid tunl token", zap.String("token", tunlToken), zap.Error(err))

						http.Error(response, err.Error(), http.StatusInternalServerError)
						return
					}

					address, err = addresses.ClaimAddress(tunlType, token.Subject)
					if err != nil {
						logger.Info("address claim error", zap.String("address", token.Subject))

						http.Error(response, err.Error(), http.StatusInternalServerError)
						return
					}

				} else {
					var err error
					address, err = addresses.NewAddress(tunlType)
					if err != nil {
						http.Error(response, err.Error(), http.StatusInternalServerError)
						return
					}
				}

				defer address.Close()

				token, err := createToken(signKey, address.Address)
				if err != nil {
					logger.Error("failed to create token", zap.Error(err))
					http.Error(response, "internal server error", http.StatusInternalServerError)
					return
				}

				accept := &http.Response{
					StatusCode: http.StatusOK,
					Header: http.Header{
						"X-Tunl-Token":   []string{token},
						"X-Tunl-Address": []string{address.Address},
						"X-Tunl-Version": []string{version.String()},
					},
				}

				if err := accept.Write(conn); err != nil {
					logger.Error("failed to write success response", zap.Error(err))
					return
				}

				session, err := yamux.Server(conn, nil)
				if err != nil {
					logger.Debug("mux server creation error", zap.Error(err))
					return
				}

				started := time.Now()
				defer func() {
					session.Close()
					logger.Debug("tunnel closed", zap.String("address", address.Address), zap.Duration("time-online", time.Since(started)))
				}()

				accepted := make(chan net.Conn)
				go func() {
					defer close(accepted)
					defer tunnelCount.Dec(1)

					tunnelCount.Inc(1)

					for {
						conn, err := address.Accept()
						if err != nil {
							logger.Debug("vhost accept error", zap.Error(err))
							return
						}

						accepted <- conn
					}
				}()

				for {
					select {
					case <-session.CloseChan():
						logger.Debug("session closed")
						return

					case conn, ok := <-accepted:
						if !ok {
							return
						}

						go func(public net.Conn) {
							defer public.Close()
							defer connectionCount.Dec(1)

							connectionCount.Inc(1)

							logger.Debug("accepted "+public.RemoteAddr().String(), zap.String("address", address.Address))

							local, err := session.Open()
							if err != nil {
								logger.Debug("failed to open session", zap.Error(err))
								return
							}
							defer local.Close()

							var work sync.WaitGroup
							work.Add(2)

							var in int64
							var out int64

							go func() {
								in, _ = io.Copy(local, public)
								work.Done()
							}()

							go func() {
								out, _ = io.Copy(public, local)
								work.Done()
							}()

							work.Wait()
							logger.Debug("connection copy finished", zap.Int64("in", in), zap.Int64("out", out), zap.Stringer("public", public.RemoteAddr()), zap.Stringer("local", local.RemoteAddr()))
						}(conn)
					}
				}
			}))
		}()

		go func() {
			for {
				conn, err := mux.NextError()
				if err != nil {
					switch err.(type) {
					case vhost.BadRequest:
						logger.Debug("vhost accept error: bad request", zap.Error(err))
						break

					case vhost.NotFound:
						logger.Error("vhost mux reached unknown host")
						(&http.Response{
							Status:     "not found",
							StatusCode: http.StatusNotFound,
						}).Write(conn)
						break

					case vhost.Closed:
						logger.Error("vhost mux reached closed host")
						(&http.Response{
							Status:     "not found",
							StatusCode: http.StatusGone,
						}).Write(conn)
						break
					default:
						logger.Debug("unknown mux error", zap.Error(err))
					}

				}

				if conn != nil {
					conn.Close()
				}
			}
		}()

		if err := <-failed; err != nil {
			logger.Error("fatal error", zap.Error(err))
		}

		return nil
	},
}
View Source
var DirCommand = &cli.Command{
	Name: "dir",
	Flags: []cli.Flag{
		CopyToClipboardFlag,
		&cli.BoolFlag{
			Name:  "access-log",
			Value: true,
		},
		&cli.StringFlag{
			Name: "password",
		},
		&cli.StringFlag{
			Name:  "basic-auth",
			Usage: "Adds HTTP basic access authentication",
		},

		&cli.BoolFlag{
			Name:  "qr",
			Usage: "Print QR code of the public address",
		},
	},
	Usage:     "Expose a directory via a public http address",
	ArgsUsage: "[dir]",
	Action: func(ctx *cli.Context) error {
		dir := ctx.Args().First()
		if len(dir) == 0 {
			dir = "."
		}

		absDir, err := filepath.Abs(dir)
		if err != nil {
			return cli.Exit("invalid dir: "+err.Error(), 1)
		}

		stat, err := os.Stat(absDir)
		if err != nil {
			if os.IsNotExist(err) {
				return cli.Exit("directory doesn't exist", 1)
			}

			return cli.Exit(err.Error(), 1)
		}

		if !stat.IsDir() {
			return cli.Exit(dir+" not a directory", 1)
		}

		host := ctx.String("host")
		if len(host) == 0 {
			fmt.Print("Host cannot be empty\nSee --host flag for more information.\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return cli.Exit("Host cannot be empty.", 1)
		}

		hostURL, err := url.Parse(host)
		if err != nil {
			fmt.Printf("Host value invalid: %v\nSee --host flag for more information.\n\n", err)

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return nil
		}

		hostnameWithoutPort := hostURL.Hostname()
		if len(hostnameWithoutPort) == 0 {
			fmt.Print("Host hostname cannot be empty, see --host flag for more information.\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return nil
		}

		handler := http.FileServer(http.Dir(absDir))

		if password := ctx.String("password"); len(password) > 0 {
			next := handler

			var store = sessions.NewCookieStore([]byte("foobar"))

			handler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
				session, _ := store.Get(r, "tunl-pass")

				if session.Values["pass"] == password {
					next.ServeHTTP(rw, r)
					return
				}

				if r.Method == http.MethodPost {
					if r.ParseForm(); err != nil {
						http.Error(rw, err.Error(), http.StatusBadRequest)
						return
					}

					pass := r.FormValue("password")
					if pass != password {
						templates.Password(rw, templates.PasswordInput{
							Message: "invalid password",
						})
						return
					}

					session.Values["pass"] = pass
					session.Save(r, rw)

					http.Redirect(rw, r, "/", http.StatusSeeOther)
					return
				}
				templates.Password(rw, templates.PasswordInput{})

			})

		}

		if basicAuth := ctx.String("basic-auth"); len(basicAuth) > 0 {
			split := strings.Split(basicAuth, ":")
			if len(split) != 2 {
				return cli.Exit("invalid basic-auth value", 1)
			}

			user := split[0]
			password := split[1]

			if len(user) == 0 {
				return cli.Exit("invalid basic-auth value: empty user", 1)
			}
			if len(password) == 0 {
				return cli.Exit("invalid basic-auth value: empty password", 1)
			}

			handler = httpauth.SimpleBasicAuth(user, password)(handler)
		}

		if ctx.Bool("access-log") {
			handler = handlers.LoggingHandler(os.Stderr, handler)
		}

		tunnel, err := tunnel.OpenHTTP(ctx.Context, zap.NewNop(), hostURL)
		if err != nil {
			return cli.Exit(err.Error(), 18)
		}

		PrintTunnel(ctx, tunnel.Address(), absDir)

		go func() {
			for {
				select {
				case state := <-tunnel.StateChanges():
					println(state)
				case version := <-tunnel.NewVersions():
					current, err := semver.NewVersion(version.String())
					if err == nil {
						if current.LessThan(&version) {
							println("new version available: " + version.String())
						}
					}
				}
			}
		}()

		if err := http.Serve(tunnel, handler); err != nil {
			return err
		}

		return nil
	},
}
View Source
var DockerCommand = &cli.Command{
	Name:      "docker",
	ArgsUsage: "<container>[:<port>]",
	Flags: []cli.Flag{
		CopyToClipboardFlag,
		&cli.BoolFlag{
			Name:  "qr",
			Usage: "Print QR code of the public address",
		},
		&cli.BoolFlag{
			Name:  "copy-address",
			Usage: "Copies the public address to the clipboard",
		},
	},
	Usage: "Expose a docker container port via a public address",
	BashComplete: cli.BashCompleteFunc(func(ctx *cli.Context) {
		docker, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
		if err != nil {
			return
		}

		args := filters.NewArgs()

		if ctx.Args().Present() {
			args.FuzzyMatch("name", ctx.Args().First())
		}

		containers, err := docker.ContainerList(context.Background(), types.ContainerListOptions{Filters: args})
		if err != nil {
			return
		}

		for _, container := range containers {
			for _, name := range container.Names {
				fmt.Println(name)
			}

			fmt.Println(container.ID)
		}
	}),
	Action: func(ctx *cli.Context) error {
		containerAndPort := ctx.Args().First()
		if len(containerAndPort) == 0 {
			fmt.Print("Missing container argument.\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return cli.Exit("Container cannot be empty.", 1)
		}

		docker, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
		if err != nil {
			panic(err)
		}

		containerSpec, portSpec, err := net.SplitHostPort(containerAndPort)
		if err != nil {
			fmt.Print("Invalid container argument\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return cli.Exit(err.Error(), 1)
		}

		container, err := docker.ContainerInspect(context.Background(), containerSpec)
		if err != nil {
			return cli.Exit(err.Error(), 1)
		}

		var port int

		if len(portSpec) > 0 {
			parsed, err := strconv.Atoi(portSpec)
			if err != nil {
				return cli.Exit("Invalid port: "+portSpec, 1)
			}
			port = parsed
		} else {
			if exposedPorts := container.Config.ExposedPorts; len(exposedPorts) > 0 {
				for exposedPort := range exposedPorts {
					port = exposedPort.Int()
				}
			} else {
				return cli.Exit("Missing port argument and no exposed ports found in container", 1)
			}
		}

		host := ctx.String("host")
		if len(host) == 0 {
			fmt.Print("Host cannot be empty\nSee --host flag for more information.\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return cli.Exit("Host cannot be empty.", 1)
		}

		hostURL, err := url.Parse(host)
		if err != nil {
			fmt.Printf("Host value invalid: %v\nSee --host flag for more information.\n\n", err)

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return nil
		}

		hostnameWithoutPort := hostURL.Hostname()
		if len(hostnameWithoutPort) == 0 {
			fmt.Print("Host hostname cannot be empty, see --host flag for more information.\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return nil
		}

		tunnel, err := tunnel.OpenTCP(ctx.Context, zap.NewNop(), hostURL)
		if err != nil {
			return cli.Exit(err.Error(), 18)
		}

		PrintTunnel(ctx, tunnel.Address(), fmt.Sprintf("%s:%v", container.Name[1:], port))

		go func() {
			for state := range tunnel.StateChanges() {
				println(state)
			}
		}()

		for {
			conn, err := tunnel.Accept()
			if err != nil {
				return cli.Exit("accept error: "+err.Error(), 1)
			}

			fmt.Println(conn.RemoteAddr())

			go func(conn net.Conn) {
				defer conn.Close()

				target, err := net.Dial("tcp", fmt.Sprintf("%s:%v", container.NetworkSettings.IPAddress, port))
				if err != nil {
					println(err.Error())
					return
				}

				var work sync.WaitGroup
				work.Add(2)

				go func() {
					defer work.Done()
					io.Copy(conn, target)
				}()

				go func() {
					defer work.Done()
					io.Copy(target, conn)
				}()

				work.Wait()
			}(conn)
		}
	},
}
View Source
var FilesCommand = &cli.Command{
	Name: "files",
	Flags: []cli.Flag{
		CopyToClipboardFlag,
		&cli.BoolFlag{
			Name:  "access-log",
			Value: true,
		},
		&cli.BoolFlag{
			Name:  "qr",
			Usage: "Print QR code of the public address",
		},
	},
	Hidden:    true,
	Usage:     "Expose a directory via a public http address",
	ArgsUsage: "[dir]",
	Action: func(ctx *cli.Context) error {
		println("FILES IS DEPRECATED AND WILL BE REMOVED SOON, USE DIR COMMAND")
		return DirCommand.Action(ctx)
	},
}
View Source
var HttpCommand = &cli.Command{
	Name: "http",
	Flags: []cli.Flag{
		CopyToClipboardFlag,
		&cli.BoolFlag{
			Name:  "access-log",
			Usage: "Print http requests in Apache Log format to stderr",
			Value: true,
		},
		&cli.StringFlag{
			Name:  "basic-auth",
			Usage: "Adds HTTP basic access authentication",
		},
		&cli.BoolFlag{
			Name:  "insecure",
			Usage: "Skip TLS verification for local address (this does not effect TLS between the tunl client and server or the public address)",
			Value: true,
		},
		&cli.BoolFlag{
			Name:  "qr",
			Usage: "Print QR code of the public address",
		},
	},
	ArgsUsage: "<url>",
	Usage:     "Expose a HTTP service via a public address",
	Action: func(ctx *cli.Context) error {
		var targetURL *url.URL
		target := ctx.Args().First()
		if len(target) == 0 {
			fmt.Fprint(os.Stderr, "You must specify the <url> argument\n\n")
			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
		}

		if !strings.Contains(target, "://") {
			if strings.HasPrefix(target, ":") {
				target = target[1:]
			}

			if port, err := strconv.Atoi(target); err == nil {
				target = fmt.Sprintf("http://localhost:%v", port)
			} else {
				target = "http://" + target
			}
		}

		parsed, err := url.Parse(target)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Invalid <url> argument value: %v\n\n", err)
			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
		}
		targetURL = parsed

		proxy := httputil.NewSingleHostReverseProxy(targetURL)

		if ctx.Bool("insecure") {
			proxy.Transport = &http.Transport{
				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
			}
		}

		originalDirector := proxy.Director

		proxy.Director = func(request *http.Request) {
			originalDirector(request)
			request.Host = targetURL.Host
		}

		host := ctx.String("host")
		if len(host) == 0 {
			fmt.Fprint(os.Stderr, "Host cannot be empty\nSee --host flag for more information.\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return cli.Exit("Host cannot be empty.", 1)
		}

		hostURL, err := url.Parse(host)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Host value invalid: %v\nSee --host flag for more information.\n\n", err)

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return nil
		}

		hostnameWithoutPort := hostURL.Hostname()
		if len(hostnameWithoutPort) == 0 {
			fmt.Fprintf(os.Stderr, "Host hostname cannot be empty, see --host flag for more information.\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return nil
		}

		tunnel, err := tunnel.OpenHTTP(ctx.Context, zap.NewNop(), hostURL)
		if err != nil {
			return cli.Exit(err.Error(), 18)
		}

		PrintTunnel(ctx, tunnel.Address(), target)

		handler := handlers.LoggingHandler(os.Stdout, proxy)

		proxy.ErrorHandler = func(response http.ResponseWriter, request *http.Request, err error) {
			hostname, _ := os.Hostname()
			if len(hostname) == 0 {
				hostname = "<unknown>"
			}

			fmt.Println(err)

			var unwrapped = err

			for next := errors.Unwrap(unwrapped); next != nil; next = errors.Unwrap(unwrapped) {
				unwrapped = next
			}

			response.WriteHeader(http.StatusBadGateway)

			templates.HttpClientError(response, templates.HttpClientErrorInput{
				RemoteAddress:     tunnel.Address(),
				LocalHostname:     hostname,
				LocalAddress:      target,
				TunlClientVersion: ctx.App.Version,
				ErrMessage:        unwrapped.Error(),
				ErrType:           fmt.Sprintf("%T", err),
				ErrDetails:        err.Error(),
				Year:              time.Now().Year(),
			})
		}

		if basicAuth := ctx.String("basic-auth"); len(basicAuth) > 0 {
			split := strings.Split(basicAuth, ":")
			if len(split) != 2 {
				return cli.Exit("invalid basic-auth value", 1)
			}

			user := split[0]
			password := split[1]

			if len(user) == 0 {
				return cli.Exit("invalid basic-auth value: empty user", 1)
			}
			if len(password) == 0 {
				return cli.Exit("invalid basic-auth value: empty password", 1)
			}

			handler = httpauth.SimpleBasicAuth(user, password)(handler)
		}

		if err := http.Serve(tunnel, handler); err != nil {
			return cli.Exit("fatal error: "+err.Error(), 1)
		}

		return nil
	},
}
View Source
var TcpCommand = &cli.Command{
	Name: "tcp",
	Flags: []cli.Flag{
		CopyToClipboardFlag,
		&cli.BoolFlag{
			Name:  "access-log",
			Value: true,
		},
	},
	Usage:     "Expose a TCP service via a public address",
	ArgsUsage: "<host:port>",
	Action: func(ctx *cli.Context) error {
		target := ctx.Args().First()
		if len(target) == 0 {
			fmt.Fprint(os.Stderr, "You must specify the <url> argument\n\n")
			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
		}

		host := ctx.String("host")
		if len(host) == 0 {
			fmt.Fprint(os.Stderr, "Host cannot be empty\nSee --host flag for more information.\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return cli.Exit("Host cannot be empty.", 1)
		}

		hostURL, err := url.Parse(host)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Host value invalid: %v\nSee --host flag for more information.\n\n", err)

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return nil
		}

		hostnameWithoutPort := hostURL.Hostname()
		if len(hostnameWithoutPort) == 0 {
			fmt.Fprintf(os.Stderr, "Host hostname cannot be empty, see --host flag for more information.\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return nil
		}

		tunnel, err := tunnel.OpenTCP(ctx.Context, zap.NewNop(), hostURL)
		if err != nil {
			return cli.Exit(err.Error(), 18)
		}

		PrintTunnel(ctx, tunnel.Address(), ctx.Args().First())

		for {
			conn, err := tunnel.Accept()
			if err != nil {
				return cli.Exit("fatal error: "+err.Error(), 1)
			}

			fmt.Println(conn.RemoteAddr())

			go func(conn net.Conn) {
				defer conn.Close()

				target, err := net.Dial("tcp", ctx.Args().First())
				if err != nil {
					println(err.Error())
					return
				}

				var work sync.WaitGroup
				work.Add(2)

				go func() {
					defer work.Done()
					io.Copy(conn, target)
				}()

				go func() {
					defer work.Done()
					io.Copy(target, conn)
				}()

				work.Wait()
			}(conn)
		}
	},
}
View Source
var VersionCommand = &cli.Command{
	Name:  "version",
	Usage: "Print version information",
	Action: func(ctx *cli.Context) error {
		cli.VersionPrinter(ctx)
		return nil
	},
}
View Source
var WebdavCommand = &cli.Command{
	Name: "webdav",
	Flags: []cli.Flag{
		CopyToClipboardFlag,
		&cli.BoolFlag{
			Name:  "access-log",
			Value: true,
		},
		&cli.StringFlag{
			Name:  "basic-auth",
			Usage: "Adds HTTP basic access authentication",
		},
		&cli.BoolFlag{
			Name:  "qr",
			Usage: "Print QR code of the public address",
		},
	},
	Usage:     "Expose a directory via a public webdav address",
	ArgsUsage: "[dir]",
	Action: func(ctx *cli.Context) error {
		dir := ctx.Args().First()
		if len(dir) == 0 {
			dir = "."
		}

		absDir, err := filepath.Abs(dir)
		if err != nil {
			return cli.Exit("invalid dir: "+err.Error(), 1)
		}

		stat, err := os.Stat(absDir)
		if err != nil {
			if os.IsNotExist(err) {
				return cli.Exit("directory doesn't exist", 1)
			}

			return cli.Exit(err.Error(), 1)
		}

		if !stat.IsDir() {
			return cli.Exit(dir+" not a directory", 1)
		}

		host := ctx.String("host")
		if len(host) == 0 {
			fmt.Print("Host cannot be empty\nSee --host flag for more information.\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return cli.Exit("Host cannot be empty.", 1)
		}

		hostURL, err := url.Parse(host)
		if err != nil {
			fmt.Printf("Host value invalid: %v\nSee --host flag for more information.\n\n", err)

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return nil
		}

		hostnameWithoutPort := hostURL.Hostname()
		if len(hostnameWithoutPort) == 0 {
			fmt.Print("Host hostname cannot be empty, see --host flag for more information.\n\n")

			cli.ShowCommandHelpAndExit(ctx, ctx.Command.Name, 1)
			return nil
		}

		handler := http.Handler(&webdav.Handler{
			FileSystem: webdav.Dir(absDir),
			LockSystem: webdav.NewMemLS(),
		})

		if basicAuth := ctx.String("basic-auth"); len(basicAuth) > 0 {
			split := strings.Split(basicAuth, ":")
			if len(split) != 2 {
				return cli.Exit("invalid basic-auth value", 1)
			}

			user := split[0]
			password := split[1]

			if len(user) == 0 {
				return cli.Exit("invalid basic-auth value: empty user", 1)
			}
			if len(password) == 0 {
				return cli.Exit("invalid basic-auth value: empty password", 1)
			}

			handler = httpauth.SimpleBasicAuth(user, password)(handler)
		}

		if ctx.Bool("access-log") {
			handler = handlers.LoggingHandler(os.Stderr, handler)
		}

		tunnel, err := tunnel.OpenHTTP(ctx.Context, zap.NewNop(), hostURL)
		if err != nil {
			return cli.Exit(err.Error(), 18)
		}

		PrintTunnel(ctx, tunnel.Address(), absDir)

		go func() {
			for state := range tunnel.StateChanges() {
				println(state)
			}
		}()

		if err := http.Serve(tunnel, handler); err != nil {
			return err
		}

		return nil
	},
}

Functions

func CopyAddressToClipboardIfRequired

func CopyAddressToClipboardIfRequired(ctx *cli.Context, address string)

func DialHost

func DialHost(ctx *cli.Context) (net.Conn, string, error)

func PrintTunnel

func PrintTunnel(ctx *cli.Context, publicAddress string, target string)

PrintTunnel prints public address to stdout and target to stderr

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL