Start implementing remote-controlled clients

This commit is contained in:
Anders Pitman 2020-10-09 10:05:31 -06:00
parent 27a487a032
commit 4cd19cb90f
10 changed files with 142 additions and 38 deletions

16
api.go
View File

@ -13,7 +13,6 @@ type Api struct {
mux *http.ServeMux
}
func NewApi(config *BoringProxyConfig, auth *Auth, tunMan *TunnelManager) *Api {
api := &Api{config, auth, tunMan, nil}
@ -34,7 +33,20 @@ func (a *Api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (a *Api) handleTunnels(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
body, err := json.Marshal(a.tunMan.GetTunnels())
query := r.URL.Query()
tunnels := a.tunMan.GetTunnels()
if len(query["client-name"]) == 1 {
clientName := query["client-name"][0]
for k, tun := range tunnels {
if tun.ClientName != clientName {
delete(tunnels, k)
}
}
}
body, err := json.Marshal(tunnels)
if err != nil {
w.WriteHeader(500)
w.Write([]byte("Error encoding tunnels"))

View File

@ -190,27 +190,35 @@ func (p *BoringProxy) handleCreateTunnel(w http.ResponseWriter, r *http.Request)
r.ParseForm()
if len(r.Form["host"]) != 1 {
if len(r.Form["domain"]) != 1 {
w.WriteHeader(400)
w.Write([]byte("Invalid host parameter"))
w.Write([]byte("Invalid domain parameter"))
return
}
host := r.Form["host"][0]
domain := r.Form["domain"][0]
if len(r.Form["port"]) != 1 {
if len(r.Form["client-name"]) != 1 {
w.WriteHeader(400)
w.Write([]byte("Invalid port parameter"))
w.Write([]byte("Invalid client-name parameter"))
return
}
clientName := r.Form["client-name"][0]
if len(r.Form["client-port"]) != 1 {
w.WriteHeader(400)
w.Write([]byte("Invalid client-port parameter"))
return
}
port, err := strconv.Atoi(r.Form["port"][0])
clientPort, err := strconv.Atoi(r.Form["client-port"][0])
if err != nil {
w.WriteHeader(400)
w.Write([]byte("Invalid port parameter"))
w.Write([]byte("Invalid client-port parameter"))
return
}
err = p.tunMan.SetTunnel(host, port)
fmt.Println(domain, clientName, clientPort)
_, err = p.tunMan.CreateTunnelForClient(domain, clientName, clientPort)
if err != nil {
w.WriteHeader(400)
io.WriteString(w, "Failed to get cert. Ensure your domain is valid")

View File

@ -90,12 +90,12 @@ func Listen() {
}
})
// taken from: https://stackoverflow.com/a/37537134/943814
go func() {
if err := http.ListenAndServe(":80", http.HandlerFunc(redirectTLS)); err != nil {
log.Fatalf("ListenAndServe error: %v", err)
}
}()
// taken from: https://stackoverflow.com/a/37537134/943814
go func() {
if err := http.ListenAndServe(":80", http.HandlerFunc(redirectTLS)); err != nil {
log.Fatalf("ListenAndServe error: %v", err)
}
}()
log.Println("BoringProxy ready")
@ -154,7 +154,7 @@ func (p *BoringProxy) proxyRequest(w http.ResponseWriter, r *http.Request) {
}
func redirectTLS(w http.ResponseWriter, r *http.Request) {
url := fmt.Sprintf("https://%s:443%s", r.Host, r.RequestURI)
log.Println("redir", url)
http.Redirect(w, r, url, http.StatusMovedPermanently)
url := fmt.Sprintf("https://%s:443%s", r.Host, r.RequestURI)
log.Println("redir", url)
http.Redirect(w, r, url, http.StatusMovedPermanently)
}

View File

@ -11,6 +11,8 @@ import (
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"sync"
)
@ -21,6 +23,84 @@ func NewBoringProxyClient() *BoringProxyClient {
return &BoringProxyClient{}
}
func (c *BoringProxyClient) RunPuppetClient() {
flagSet := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
server := flagSet.String("server", "", "boringproxy server")
token := flagSet.String("token", "", "Access token")
name := flagSet.String("client-name", "", "Client name")
flagSet.Parse(os.Args[2:])
httpClient := &http.Client{}
url := fmt.Sprintf("https://%s/api/tunnels?client-name=%s", *server, *name)
listenReq, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Fatal("Failed making request", err)
}
if len(*token) > 0 {
listenReq.Header.Add("Authorization", "bearer "+*token)
}
resp, err := httpClient.Do(listenReq)
if err != nil {
log.Fatal("Failed make tunnel request", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if resp.StatusCode != 200 {
log.Fatal("Failed to create tunnel: " + string(body))
}
tunnels := make(map[string]Tunnel)
err = json.Unmarshal(body, &tunnels)
if err != nil {
log.Fatal("Failed to parse response", err)
}
for _, tun := range tunnels {
go c.BoreTunnel(tun)
}
//go c.BoreTunnel(tunnels["apitman.com"])
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
for range sigChan {
break
}
}
func (c *BoringProxyClient) BoreTunnel(tun Tunnel) {
privKeyFile, err := ioutil.TempFile("", "")
if err != nil {
log.Fatal(err)
}
defer os.Remove(privKeyFile.Name())
if _, err := privKeyFile.Write([]byte(tun.TunnelPrivateKey)); err != nil {
log.Fatal(err)
}
if err := privKeyFile.Close(); err != nil {
log.Fatal(err)
}
tunnelSpec := fmt.Sprintf("127.0.0.1:%d:127.0.0.1:%d", tun.TunnelPort, tun.ClientPort)
sshLogin := fmt.Sprintf("%s@%s", tun.Username, tun.ServerAddress)
serverPortStr := fmt.Sprintf("%d", tun.ServerPort)
fmt.Println(tunnelSpec, sshLogin, serverPortStr)
cmd := exec.Command("ssh", "-i", privKeyFile.Name(), "-NR", tunnelSpec, sshLogin, "-p", serverPortStr)
err = cmd.Run()
if err != nil {
log.Fatal(err)
}
}
func (c *BoringProxyClient) Run() {
log.Println("Run client")

View File

@ -24,9 +24,10 @@ type Tunnel struct {
Username string `json:"username"`
TunnelPort int `json:"tunnel_port"`
TunnelPrivateKey string `json:"tunnel_private_key"`
ClientName string `json:"client_name"`
ClientPort int `json:"client_port"`
}
func NewDatabase() (*Database, error) {
dbJson, err := ioutil.ReadFile("boringproxy_db.json")

View File

@ -22,7 +22,7 @@ func main() {
case "client":
client := NewBoringProxyClient()
client.Run()
client.RunPuppetClient()
default:
fmt.Println("Invalid command " + command)
os.Exit(1)

View File

@ -1,8 +1,7 @@
* I don't think it's properly closing connections. Browser are hanging on
some requests, possibly because it's HTTP/1.1 and hitting the max concurrent
requests.
* Might want to proxy requests at the HTTP level since it lets us do things
like terminating HTTP/2.
* Implement a custom SSH server in Go and connect the sockets directly?
* Use HTML redirects for showing errors then refreshing. Maybe for polling
after login and submitting a new tunnel too.
* Save next port in db

View File

@ -32,4 +32,4 @@ ssh -i $keyFile \
echo "Cleaning up"
rm $keyFile
curl -s -H "Authorization: bearer $token" -X DELETE "$api/tunnels?domain=$domain"
#curl -s -H "Authorization: bearer $token" -X DELETE "$api/tunnels?domain=$domain"

View File

@ -17,7 +17,7 @@ import (
)
type TunnelManager struct {
config *BoringProxyConfig
config *BoringProxyConfig
db *Database
nextPort int
mutex *sync.Mutex
@ -50,18 +50,20 @@ func (m *TunnelManager) GetTunnels() map[string]Tunnel {
return m.db.GetTunnels()
}
// TODO: Update this
func (m *TunnelManager) SetTunnel(host string, port int) error {
err := m.certConfig.ManageSync([]string{host})
func (m *TunnelManager) CreateTunnelForClient(domain string, clientName string, clientPort int) (Tunnel, error) {
tun, err := m.CreateTunnel(domain)
if err != nil {
log.Println(err)
return errors.New("Failed to get cert")
return Tunnel{}, err
}
tunnel := Tunnel{TunnelPort: port}
m.db.SetTunnel(host, tunnel)
tun.ClientName = clientName
tun.ClientPort = clientPort
return nil
// TODO: It's a bit hacky that we call db.SetTunnel in CreateTunnel and
// then again here
m.db.SetTunnel(domain, tun)
return tun, nil
}
func (m *TunnelManager) CreateTunnel(domain string) (Tunnel, error) {
@ -87,7 +89,7 @@ func (m *TunnelManager) CreateTunnel(domain string) (Tunnel, error) {
return Tunnel{}, err
}
tunnel := Tunnel{
tunnel := Tunnel{
ServerAddress: m.config.AdminDomain,
ServerPort: 22,
ServerPublicKey: "",

View File

@ -48,7 +48,7 @@
{{range $domain, $tunnel:= .}}
<div class='tunnel'>
<div>
<a href="https://{{$domain}}">{{$domain}}</a> -> {{$tunnel.TunnelPort}}
<a href="https://{{$domain}}">{{$domain}}</a>:{{$tunnel.TunnelPort}} -> {{$tunnel.ClientName}}:{{$tunnel.ClientPort}}
</div>
<a href="/delete-tunnel?host={{$domain}}">Delete</a>
</div>
@ -57,9 +57,11 @@
<div class='tunnel-adder'>
<form action="/tunnels" method="POST">
<label for="domain">Domain:</label>
<input type="text" id="domain" name="host">
<label for="port">Port:</label>
<input type="text" id="port" name="port">
<input type="text" id="domain" name="domain">
<label for="client-name">Client Name:</label>
<input type="text" id="client-name" name="client-name">
<label for="client-port">Client Port:</label>
<input type="text" id="client-port" name="client-port">
<button type="submit">Add/Update Tunnel</button>
</form>
</div>