mirror of
https://github.com/boringproxy/boringproxy.git
synced 2025-02-25 18:55:29 -06:00
Implement using custom SSH keys
This commit is contained in:
54
api.go
54
api.go
@@ -62,14 +62,23 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err
|
||||
return nil, errors.New("Invalid domain parameter")
|
||||
}
|
||||
|
||||
clientName := params.Get("client-name")
|
||||
if clientName == "" {
|
||||
return nil, errors.New("Invalid client-name parameter")
|
||||
sshKeyId := params.Get("ssh-key-id")
|
||||
|
||||
sshKey, exists := a.db.GetSshKey(sshKeyId)
|
||||
if !exists {
|
||||
return nil, errors.New("SSH key does not exist")
|
||||
}
|
||||
|
||||
clientPort, err := strconv.Atoi(params.Get("client-port"))
|
||||
if err != nil {
|
||||
return nil, errors.New("Invalid client-port parameter")
|
||||
clientName := params.Get("client-name")
|
||||
|
||||
clientPort := 0
|
||||
clientPortParam := params.Get("client-port")
|
||||
if clientPortParam != "" {
|
||||
var err error
|
||||
clientPort, err = strconv.Atoi(clientPortParam)
|
||||
if err != nil {
|
||||
return nil, errors.New("Invalid client-port parameter")
|
||||
}
|
||||
}
|
||||
|
||||
clientAddr := params.Get("client-addr")
|
||||
@@ -97,6 +106,7 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err
|
||||
|
||||
request := Tunnel{
|
||||
Domain: domain,
|
||||
SshKey: sshKey.Key,
|
||||
Owner: tokenData.Owner,
|
||||
ClientName: clientName,
|
||||
ClientPort: clientPort,
|
||||
@@ -237,6 +247,38 @@ func (a *Api) handleDeleteTunnel(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Api) GetSshKeys(tokenData TokenData) map[string]SshKey {
|
||||
|
||||
user, _ := a.db.GetUser(tokenData.Owner)
|
||||
|
||||
var keys map[string]SshKey
|
||||
|
||||
if user.IsAdmin {
|
||||
keys = a.db.GetSshKeys()
|
||||
} else {
|
||||
keys = make(map[string]SshKey)
|
||||
|
||||
for id, key := range a.db.GetSshKeys() {
|
||||
if tokenData.Owner == key.Owner {
|
||||
keys[id] = key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func (a *Api) DeleteSshKey(tokenData TokenData, params url.Values) error {
|
||||
id := params.Get("id")
|
||||
if id == "" {
|
||||
return errors.New("Invalid id parameter")
|
||||
}
|
||||
|
||||
a.db.DeleteSshKey(id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Api) validateToken(h http.Handler) http.Handler {
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
63
database.go
63
database.go
@@ -12,6 +12,7 @@ type Database struct {
|
||||
Tokens map[string]TokenData `json:"tokens"`
|
||||
Tunnels map[string]Tunnel `json:"tunnels"`
|
||||
Users map[string]User `json:"users"`
|
||||
SshKeys map[string]SshKey `json:"ssh_keys"`
|
||||
mutex *sync.Mutex
|
||||
}
|
||||
|
||||
@@ -23,9 +24,15 @@ type User struct {
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
|
||||
type SshKey struct {
|
||||
Owner string `json:"owner"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type Tunnel struct {
|
||||
Owner string `json:"owner"`
|
||||
Domain string `json:"domain"`
|
||||
SshKey string `json:"ssh_key"`
|
||||
ServerAddress string `json:"server_address"`
|
||||
ServerPort int `json:"server_port"`
|
||||
ServerPublicKey string `json:"server_public_key"`
|
||||
@@ -69,6 +76,10 @@ func NewDatabase() (*Database, error) {
|
||||
db.Users = make(map[string]User)
|
||||
}
|
||||
|
||||
if db.SshKeys == nil {
|
||||
db.SshKeys = make(map[string]SshKey)
|
||||
}
|
||||
|
||||
db.mutex = &sync.Mutex{}
|
||||
|
||||
db.mutex.Lock()
|
||||
@@ -239,6 +250,58 @@ func (d *Database) DeleteUser(username string) {
|
||||
d.persist()
|
||||
}
|
||||
|
||||
func (d *Database) GetSshKey(id string) (SshKey, bool) {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
key, exists := d.SshKeys[id]
|
||||
|
||||
if !exists {
|
||||
return SshKey{}, false
|
||||
}
|
||||
|
||||
return key, true
|
||||
}
|
||||
|
||||
func (d *Database) GetSshKeys() map[string]SshKey {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
keys := make(map[string]SshKey)
|
||||
|
||||
for k, v := range d.SshKeys {
|
||||
keys[k] = v
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func (d *Database) AddSshKey(id string, key SshKey) error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
_, exists := d.SshKeys[id]
|
||||
|
||||
if exists {
|
||||
return errors.New("SSH key id exists")
|
||||
}
|
||||
|
||||
d.SshKeys[id] = key
|
||||
|
||||
d.persist()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) DeleteSshKey(id string) {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
delete(d.SshKeys, id)
|
||||
|
||||
d.persist()
|
||||
}
|
||||
|
||||
func (d *Database) persist() {
|
||||
saveJson(d, "boringproxy_db.json")
|
||||
}
|
||||
|
||||
7
todo.md
7
todo.md
@@ -11,3 +11,10 @@
|
||||
* Pretty sure we need to be mutex-locking the cancelFunc calls
|
||||
* Getting new certs isn't working behind Cloudflare. Might be able to fix by
|
||||
using the HTTP challenge and allowing HTTP on the Cloudflare side.
|
||||
* I think it's possible to create tokens for arbitrary user, even if you're not
|
||||
that user.
|
||||
* Invalid database is wiping out tunnels
|
||||
* OpenSSH server only picks up the first copy of each key. Will probably need
|
||||
to manually combine them for custom keys.
|
||||
* Send public key back to clients, so they can automatically try to find the
|
||||
matching private key.
|
||||
|
||||
@@ -77,7 +77,7 @@ func (m *TunnelManager) RequestCreateTunnel(tunReq Tunnel) (Tunnel, error) {
|
||||
return Tunnel{}, err
|
||||
}
|
||||
|
||||
privKey, err := m.addToAuthorizedKeys(tunReq.Domain, port, tunReq.AllowExternalTcp)
|
||||
privKey, err := m.addToAuthorizedKeys(tunReq.Domain, port, tunReq.AllowExternalTcp, tunReq.SshKey)
|
||||
if err != nil {
|
||||
return Tunnel{}, err
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func (m *TunnelManager) GetPort(domain string) (int, error) {
|
||||
return tunnel.TunnelPort, nil
|
||||
}
|
||||
|
||||
func (m *TunnelManager) addToAuthorizedKeys(domain string, port int, allowExternalTcp bool) (string, error) {
|
||||
func (m *TunnelManager) addToAuthorizedKeys(domain string, port int, allowExternalTcp bool, sshKey string) (string, error) {
|
||||
|
||||
authKeysPath := fmt.Sprintf("%s/.ssh/authorized_keys", m.user.HomeDir)
|
||||
|
||||
@@ -159,9 +159,19 @@ func (m *TunnelManager) addToAuthorizedKeys(domain string, port int, allowExtern
|
||||
|
||||
akStr := string(akBytes)
|
||||
|
||||
pubKey, privKey, err := MakeSSHKeyPair()
|
||||
if err != nil {
|
||||
return "", err
|
||||
var privKey string
|
||||
var pubKey string
|
||||
|
||||
if sshKey == "" {
|
||||
pubKey, privKey, err = MakeSSHKeyPair()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pubKey = strings.TrimSpace(pubKey)
|
||||
} else {
|
||||
privKey = ""
|
||||
pubKey = sshKey
|
||||
}
|
||||
|
||||
bindAddr := "127.0.0.1"
|
||||
@@ -173,9 +183,7 @@ func (m *TunnelManager) addToAuthorizedKeys(domain string, port int, allowExtern
|
||||
|
||||
tunnelId := fmt.Sprintf("boringproxy-%s-%d", domain, port)
|
||||
|
||||
pubKeyNoNewline := pubKey[:len(pubKey)-1]
|
||||
newAk := fmt.Sprintf("%s%s %s %s\n", akStr, options, pubKeyNoNewline, tunnelId)
|
||||
//newAk := fmt.Sprintf("%s%s %s%d\n", akStr, pubKeyNoNewline, "boringproxy-", port)
|
||||
newAk := fmt.Sprintf("%s%s %s %s\n", akStr, options, pubKey, tunnelId)
|
||||
|
||||
err = ioutil.WriteFile(authKeysPath, []byte(newAk), 0600)
|
||||
if err != nil {
|
||||
|
||||
@@ -34,6 +34,7 @@ type IndexData struct {
|
||||
Head template.HTML
|
||||
Tunnels map[string]Tunnel
|
||||
Tokens map[string]TokenData
|
||||
SshKeys map[string]SshKey
|
||||
Users map[string]User
|
||||
IsAdmin bool
|
||||
QrCodes map[string]template.URL
|
||||
@@ -260,6 +261,7 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
|
||||
Head: h.headHtml,
|
||||
Tunnels: tunnels,
|
||||
Tokens: tokens,
|
||||
SshKeys: h.api.GetSshKeys(tokenData),
|
||||
Users: users,
|
||||
IsAdmin: user.IsAdmin,
|
||||
QrCodes: qrCodes,
|
||||
@@ -312,14 +314,27 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
//http.Redirect(w, r, "/#/tunnels", 307)
|
||||
|
||||
case "/tokens":
|
||||
h.handleTokens(w, r, user, tokenData)
|
||||
case "/confirm-delete-token":
|
||||
h.confirmDeleteToken(w, r)
|
||||
case "/delete-token":
|
||||
h.deleteToken(w, r)
|
||||
case "/ssh-keys":
|
||||
h.handleSshKeys(w, r, user, tokenData)
|
||||
case "/delete-ssh-key":
|
||||
|
||||
r.ParseForm()
|
||||
|
||||
err := h.api.DeleteSshKey(tokenData, r.Form)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
h.alertDialog(w, r, err.Error(), "/#/ssh-keys")
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/#/ssh-keys", 303)
|
||||
|
||||
case "/confirm-logout":
|
||||
tmpl, err := h.loadTemplate("confirm.tmpl")
|
||||
if err != nil {
|
||||
@@ -391,6 +406,49 @@ func (h *WebUiHandler) handleTokens(w http.ResponseWriter, r *http.Request, user
|
||||
http.Redirect(w, r, "/#/tokens", 303)
|
||||
}
|
||||
|
||||
func (h *WebUiHandler) handleSshKeys(w http.ResponseWriter, r *http.Request, user User, tokenData TokenData) {
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
h.alertDialog(w, r, "Invalid method for /ssh-keys", "/#/ssh-keys")
|
||||
return
|
||||
}
|
||||
|
||||
r.ParseForm()
|
||||
|
||||
id := r.Form.Get("id")
|
||||
if id == "" {
|
||||
w.WriteHeader(400)
|
||||
h.alertDialog(w, r, "Invalid id parameter", "/#/ssh-keys")
|
||||
return
|
||||
}
|
||||
|
||||
keyParam := r.Form.Get("key")
|
||||
if keyParam == "" {
|
||||
w.WriteHeader(400)
|
||||
h.alertDialog(w, r, "Invalid key parameter", "/#/ssh-keys")
|
||||
return
|
||||
}
|
||||
|
||||
keyParam = strings.TrimSpace(keyParam)
|
||||
parts := strings.Split(keyParam, " ")
|
||||
|
||||
if len(parts) > 2 {
|
||||
keyParam = strings.Join(parts[:2], " ")
|
||||
}
|
||||
|
||||
key := SshKey{Owner: tokenData.Owner, Key: keyParam}
|
||||
|
||||
err := h.db.AddSshKey(id, key)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
h.alertDialog(w, r, err.Error(), "/#/ssh-keys")
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/#/ssh-keys", 303)
|
||||
}
|
||||
|
||||
func (h *WebUiHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method != "GET" {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<div class='menu'>
|
||||
<a class='menu-item active-tab' href='/#/tunnels'>Tunnels</a>
|
||||
<a class='menu-item' href='/#/tokens'>Tokens</a>
|
||||
<a class='menu-item' href='/#/ssh-keys'>SSH Keys</a>
|
||||
{{if .IsAdmin}}
|
||||
<a class='menu-item' href='/#/users'>Users</a>
|
||||
{{end}}
|
||||
@@ -60,9 +61,20 @@
|
||||
<label for="domain">Domain:</label>
|
||||
<input type="text" id="domain" name="domain" required>
|
||||
</div>
|
||||
|
||||
<div class='input'>
|
||||
<label for="ssh-key-id-select">SSH Key:</label>
|
||||
<select id="ssh-key-id-select" name="ssh-key-id">
|
||||
<option value="generate">Generate</option>
|
||||
{{range $id, $sshKey := $.SshKeys}}
|
||||
<option value="{{$id}}">{{$id}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class='input'>
|
||||
<label for="client-name">Client Name:</label>
|
||||
<input type="text" id="client-name" name="client-name" required>
|
||||
<input type="text" id="client-name" name="client-name">
|
||||
</div>
|
||||
<div class='input'>
|
||||
<label for="client-addr">Client Address:</label>
|
||||
@@ -70,7 +82,7 @@
|
||||
</div>
|
||||
<div class='input'>
|
||||
<label for="client-port">Client Port:</label>
|
||||
<input type="text" id="client-port" name="client-port" required>
|
||||
<input type="text" id="client-port" name="client-port">
|
||||
</div>
|
||||
<div class='input'>
|
||||
<label for="allow-external-tcp">Allow External TCP:</label>
|
||||
@@ -97,6 +109,7 @@
|
||||
<div class='menu'>
|
||||
<a class='menu-item' href='/#/tunnels'>Tunnels</a>
|
||||
<a class='menu-item active-tab' href='/#/tokens'>Tokens</a>
|
||||
<a class='menu-item' href='/#/ssh-keys'>SSH Keys</a>
|
||||
{{if .IsAdmin}}
|
||||
<a class='menu-item' href='/#/users'>Users</a>
|
||||
{{end}}
|
||||
@@ -119,8 +132,8 @@
|
||||
|
||||
<div class='token-adder'>
|
||||
<form action="/tokens" method="POST">
|
||||
<label for="owner">Owner:</label>
|
||||
<select id="owner" name="owner">
|
||||
<label for="token-owner">Owner:</label>
|
||||
<select id="token-owner" name="owner">
|
||||
{{range $username, $user := .Users}}
|
||||
<option value="{{$username}}">{{$username}}</option>
|
||||
{{end}}
|
||||
@@ -131,6 +144,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='page' id='/ssh-keys'>
|
||||
<div class='menu'>
|
||||
<a class='menu-item' href='/#/tunnels'>Tunnels</a>
|
||||
<a class='menu-item' href='/#/tokens'>Tokens</a>
|
||||
<a class='menu-item active-tab' href='/#/ssh-keys'>SSH Keys</a>
|
||||
{{if .IsAdmin}}
|
||||
<a class='menu-item' href='/#/users'>Users</a>
|
||||
{{end}}
|
||||
<a class='menu-item' href='/confirm-logout'>Logout</a>
|
||||
</div>
|
||||
<div class='content'>
|
||||
<div class='list'>
|
||||
{{range $id, $sshKey := .SshKeys}}
|
||||
|
||||
<div class='list-item'>
|
||||
<span class='monospace'>{{$id}} ({{$sshKey.Owner}})</span>
|
||||
<div class='monospace'>{{$sshKey.Key}}</div>
|
||||
<a href="/delete-ssh-key?id={{$id}}">
|
||||
<button class='button'>Delete</button>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class='ssh-key-adder'>
|
||||
<h1>Add SSH Key</h1>
|
||||
<form action="/ssh-keys" method="POST">
|
||||
<div class='input'>
|
||||
<label for="ssh-key-id">SSH Key ID:</label>
|
||||
<input type="text" id="ssh-key-id" name="id" required>
|
||||
</div>
|
||||
<div class='input'>
|
||||
<label for="ssh-key-owner">Owner:</label>
|
||||
<select id="ssh-key-owner" name="owner">
|
||||
{{range $username, $user := .Users}}
|
||||
<option value="{{$username}}">{{$username}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class='input'>
|
||||
<label for="ssh-key">SSH Key:</label>
|
||||
<textarea id='ssh-key' name='key' rows='8' cols='30'>
|
||||
</textarea>
|
||||
</div>
|
||||
<button class='button' type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .IsAdmin}}
|
||||
<div class='page' id='/users'>
|
||||
<div class='menu'>
|
||||
|
||||
@@ -151,6 +151,12 @@ main {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ssh-key-adder form {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: .7em;
|
||||
margin: .2em;
|
||||
@@ -181,6 +187,10 @@ main {
|
||||
font-family: Monospace;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: Monospace;
|
||||
}
|
||||
|
||||
.page {
|
||||
margin-top: var(--menu-label-height);
|
||||
display: none;
|
||||
|
||||
Reference in New Issue
Block a user