Implement password-protected tunnels

This commit is contained in:
Anders Pitman 2020-10-20 20:03:59 -06:00
parent 85a5004cc7
commit fd0451fa3b
6 changed files with 69 additions and 20 deletions

18
api.go
View File

@ -79,6 +79,22 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err
allowExternalTcp := params.Get("allow-external-tcp") == "on" allowExternalTcp := params.Get("allow-external-tcp") == "on"
passwordProtect := params.Get("password-protect") == "on"
var username string
var password string
if passwordProtect {
username = params.Get("username")
if username == "" {
return nil, errors.New("Username required")
}
password = params.Get("password")
if password == "" {
return nil, errors.New("Password required")
}
}
request := Tunnel{ request := Tunnel{
Domain: domain, Domain: domain,
Owner: tokenData.Owner, Owner: tokenData.Owner,
@ -86,6 +102,8 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err
ClientPort: clientPort, ClientPort: clientPort,
ClientAddress: clientAddr, ClientAddress: clientAddr,
AllowExternalTcp: allowExternalTcp, AllowExternalTcp: allowExternalTcp,
AuthUsername: username,
AuthPassword: password,
} }
tunnel, err := a.tunMan.RequestCreateTunnel(request) tunnel, err := a.tunMan.RequestCreateTunnel(request)

View File

@ -26,6 +26,7 @@ type SmtpConfig struct {
} }
type BoringProxy struct { type BoringProxy struct {
db *Database
tunMan *TunnelManager tunMan *TunnelManager
httpClient *http.Client httpClient *http.Client
} }
@ -83,7 +84,7 @@ func Listen() {
httpClient := &http.Client{} httpClient := &http.Client{}
p := &BoringProxy{tunMan, httpClient} p := &BoringProxy{db, tunMan, httpClient}
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
GetCertificate: certConfig.GetCertificate, GetCertificate: certConfig.GetCertificate,
@ -117,17 +118,34 @@ func Listen() {
func (p *BoringProxy) proxyRequest(w http.ResponseWriter, r *http.Request) { func (p *BoringProxy) proxyRequest(w http.ResponseWriter, r *http.Request) {
port, err := p.tunMan.GetPort(r.Host) tunnel, exists := p.db.GetTunnel(r.Host)
if err != nil { if !exists {
errMessage := fmt.Sprintf("No tunnel attached to %s", r.Host) errMessage := fmt.Sprintf("No tunnel attached to %s", r.Host)
w.WriteHeader(500) w.WriteHeader(500)
io.WriteString(w, errMessage) io.WriteString(w, errMessage)
return return
} }
if tunnel.AuthUsername != "" || tunnel.AuthPassword != "" {
username, password, ok := r.BasicAuth()
if !ok {
w.Header()["WWW-Authenticate"] = []string{"Basic"}
w.WriteHeader(401)
return
}
if username != tunnel.AuthUsername || password != tunnel.AuthPassword {
w.Header()["WWW-Authenticate"] = []string{"Basic"}
w.WriteHeader(401)
// TODO: should probably use a better form of rate limiting
time.Sleep(2 * time.Second)
return
}
}
downstreamReqHeaders := r.Header.Clone() downstreamReqHeaders := r.Header.Clone()
upstreamAddr := fmt.Sprintf("localhost:%d", port) upstreamAddr := fmt.Sprintf("localhost:%d", tunnel.TunnelPort)
upstreamUrl := fmt.Sprintf("http://%s%s", upstreamAddr, r.URL.RequestURI()) upstreamUrl := fmt.Sprintf("http://%s%s", upstreamAddr, r.URL.RequestURI())
upstreamReq, err := http.NewRequest(r.Method, upstreamUrl, r.Body) upstreamReq, err := http.NewRequest(r.Method, upstreamUrl, r.Body)

View File

@ -36,6 +36,8 @@ type Tunnel struct {
ClientAddress string `json:"client_address"` ClientAddress string `json:"client_address"`
ClientPort int `json:"client_port"` ClientPort int `json:"client_port"`
AllowExternalTcp bool `json:"allow_external_tcp"` AllowExternalTcp bool `json:"allow_external_tcp"`
AuthUsername string `json:"auth_username"`
AuthPassword string `json:"auth_password"`
CssId string `json:"css_id"` CssId string `json:"css_id"`
} }

View File

@ -82,24 +82,16 @@ func (m *TunnelManager) RequestCreateTunnel(tunReq Tunnel) (Tunnel, error) {
return Tunnel{}, err return Tunnel{}, err
} }
tunnel := Tunnel{ tunReq.ServerAddress = m.config.WebUiDomain
Owner: tunReq.Owner, tunReq.ServerPort = 22
Domain: tunReq.Domain, tunReq.ServerPublicKey = ""
ServerAddress: m.config.WebUiDomain, tunReq.Username = m.user.Username
ServerPort: 22, tunReq.TunnelPort = port
ServerPublicKey: "", tunReq.TunnelPrivateKey = privKey
Username: m.user.Username,
TunnelPort: port,
TunnelPrivateKey: privKey,
ClientName: tunReq.ClientName,
ClientPort: tunReq.ClientPort,
ClientAddress: tunReq.ClientAddress,
AllowExternalTcp: tunReq.AllowExternalTcp,
}
m.db.SetTunnel(tunReq.Domain, tunnel) m.db.SetTunnel(tunReq.Domain, tunReq)
return tunnel, nil return tunReq, nil
} }
func (m *TunnelManager) DeleteTunnel(domain string) error { func (m *TunnelManager) DeleteTunnel(domain string) error {

View File

@ -76,6 +76,17 @@
<label for="allow-external-tcp">Allow External TCP:</label> <label for="allow-external-tcp">Allow External TCP:</label>
<input type="checkbox" id="allow-external-tcp" name="allow-external-tcp"> <input type="checkbox" id="allow-external-tcp" name="allow-external-tcp">
</div> </div>
<div class='input'>
<label for="password-protect">Password Protect:</label>
<input type="checkbox" id="password-protect" name="password-protect">
<div id='login-inputs'>
<label for="username">Username:</label>
<input type="text" id="username" name="username">
<label for="password">Password:</label>
<input type="password" id="password" name="password">
</div>
</div>
<button class='button' type="submit">Submit</button> <button class='button' type="submit">Submit</button>
</form> </form>
</div> </div>

View File

@ -164,6 +164,14 @@ main {
font-weight: bold; font-weight: bold;
} }
#login-inputs {
display: none;
}
#password-protect:checked ~ #login-inputs {
display: block;
}
.token-adder { .token-adder {
padding: 5px; padding: 5px;
display: flex; display: flex;