From 4dadaac905420bbf7329e1835123a64d3133deef Mon Sep 17 00:00:00 2001
From: Eric Larssen <elarssen91@gmail.com>
Date: Sat, 13 Aug 2016 09:32:11 -0500
Subject: [PATCH] Add basic auth to dashboard

---
 README.md                      | 19 ++++++++-------
 README_zh.md                   |  5 +++-
 conf/frps.ini                  |  3 +++
 src/models/server/config.go    | 38 +++++++++++++++++++----------
 src/models/server/dashboard.go | 44 +++++++++++++++++++++++++++++++++-
 5 files changed, 86 insertions(+), 23 deletions(-)

diff --git a/README.md b/README.md
index a46fdc8e..43c0286c 100644
--- a/README.md
+++ b/README.md
@@ -6,14 +6,14 @@
 
 ## What is frp?
 
-frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet.Now, it supports tcp, http and https protocol when requests can be forwarded by domains to backward web services.
+frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet. Now, it supports tcp, http and https protocol when requests can be forwarded by domains to backward web services.
 
 ## Catalog
 
 * [What can I do with frp?](#what-can-i-do-with-frp)
 * [Status](#status)
 * [Architecture](#architecture)
-* [Example Usage](#example-usage) 
+* [Example Usage](#example-usage)
   * [Communicate with your computer in LAN by SSH](#communicate-with-your-computer-in-lan-by-ssh)
   * [Visit your web service in LAN by custom domains](#visit-your-web-service-in-lan-by-custom-domains)
 * [Features](#features)
@@ -37,9 +37,9 @@ frp is a fast reverse proxy to help you expose a local server behind a NAT or fi
 
 ## Status
 
-frp is under development and you can try it with latest release version.Master branch for releasing stable version when dev branch for developing.
+frp is under development and you can try it with latest release version. Master branch for releasing stable version when dev branch for developing.
 
-**We may change any protocol and can't promise backward compatible.Please check the release log when upgrading.**
+**We may change any protocol and can't promise backward compatible. Please check the release log when upgrading.**
 
 ## Architecture
 
@@ -149,9 +149,12 @@ Configure a port for dashboard to enable this feature:
 ```ini
 [common]
 dashboard_port = 7500
+# dashboard's username and password are both optional,if not set, default is admin.
+dashboard_username = abc
+dashboard_password = abc
 ```
 
-Then visit `http://[server_addr]:7500` to see dashboard.
+Then visit `http://[server_addr]:7500` to see dashboard, default username and password are both `admin`.
 
 ![dashboard](/doc/pic/dashboard.png)
 
@@ -286,9 +289,9 @@ This feature is fit for a large number of short connections.
 
 2. Enable and specify the number of connection pool:
 
-  ```ini 
+  ```ini
   # frpc.ini
-  [ssh] 
+  [ssh]
   type = tcp
   local_port = 22
   pool_count = 10
@@ -300,7 +303,7 @@ When forwarding to a local port, frp does not modify the tunneled HTTP requests
 
 ```ini                                                             
 # frpc.ini                                                         
-[web] 
+[web]
 privilege_mode = true
 type = http
 local_port = 80
diff --git a/README_zh.md b/README_zh.md
index 364592f4..c602a378 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -146,9 +146,12 @@ frp 目前正在前期开发阶段,master 分支用于发布稳定版本,dev
 ```ini
 [common]
 dashboard_port = 7500
+# dashboard 用户名密码可选,默认都为 admin
+dashboard_username = abc
+dashboard_password = abc
 ```
 
-打开浏览器通过 `http://[server_addr]:7500` 访问 dashboard 界面。
+打开浏览器通过 `http://[server_addr]:7500` 访问 dashboard 界面,用户名密码默认为 `admin`。
 
 ![dashboard](/doc/pic/dashboard.png)
 
diff --git a/conf/frps.ini b/conf/frps.ini
index 4a6d47f8..6163bc98 100644
--- a/conf/frps.ini
+++ b/conf/frps.ini
@@ -9,6 +9,9 @@ vhost_http_port = 80
 vhost_https_port = 443
 # if you want to configure or reload frps by dashboard, dashboard_port must be set
 dashboard_port = 7500
+# dashboard username and password for basic protect, if not set, both default value is admin
+dashboard_username = abc
+dashboard_password = abc
 # dashboard assets directory(only for debug mode)
 # assets_dir = ./static
 # console or real logFile path like ./frps.log
diff --git a/src/models/server/config.go b/src/models/server/config.go
index e3296d05..58ae0da3 100644
--- a/src/models/server/config.go
+++ b/src/models/server/config.go
@@ -30,19 +30,21 @@ import (
 
 // common config
 var (
-	ConfigFile     string = "./frps.ini"
-	BindAddr       string = "0.0.0.0"
-	BindPort       int64  = 7000
-	VhostHttpPort  int64  = 0 // if VhostHttpPort equals 0, don't listen a public port for http protocol
-	VhostHttpsPort int64  = 0 // if VhostHttpsPort equals 0, don't listen a public port for https protocol
-	DashboardPort  int64  = 0 // if DashboardPort equals 0, dashboard is not available
-	AssetsDir      string = ""
-	LogFile        string = "console"
-	LogWay         string = "console" // console or file
-	LogLevel       string = "info"
-	LogMaxDays     int64  = 3
-	PrivilegeMode  bool   = false
-	PrivilegeToken string = ""
+	ConfigFile        string = "./frps.ini"
+	BindAddr          string = "0.0.0.0"
+	BindPort          int64  = 7000
+	VhostHttpPort     int64  = 0 // if VhostHttpPort equals 0, don't listen a public port for http protocol
+	VhostHttpsPort    int64  = 0 // if VhostHttpsPort equals 0, don't listen a public port for https protocol
+	DashboardPort     int64  = 0 // if DashboardPort equals 0, dashboard is not available
+	DashboardUsername string = "admin"
+	DashboardPassword string = "admin"
+	AssetsDir         string = ""
+	LogFile           string = "console"
+	LogWay            string = "console" // console or file
+	LogLevel          string = "info"
+	LogMaxDays        int64  = 3
+	PrivilegeMode     bool   = false
+	PrivilegeToken    string = ""
 
 	// if PrivilegeAllowPorts is not nil, tcp proxies which remote port exist in this map can be connected
 	PrivilegeAllowPorts map[int64]struct{}
@@ -119,6 +121,16 @@ func loadCommonConf(confFile string) error {
 		DashboardPort = 0
 	}
 
+	tmpStr, ok = conf.Get("common", "dashboard_username")
+	if ok {
+		DashboardUsername = tmpStr
+	}
+
+	tmpStr, ok = conf.Get("common", "dashboard_password")
+	if ok {
+		DashboardPassword = tmpStr
+	}
+
 	tmpStr, ok = conf.Get("common", "assets_dir")
 	if ok {
 		AssetsDir = tmpStr
diff --git a/src/models/server/dashboard.go b/src/models/server/dashboard.go
index 4be7a6de..8cb201b8 100644
--- a/src/models/server/dashboard.go
+++ b/src/models/server/dashboard.go
@@ -15,9 +15,11 @@
 package server
 
 import (
+	"encoding/base64"
 	"fmt"
 	"net"
 	"net/http"
+	"strings"
 	"time"
 
 	"github.com/fatedier/frp/src/assets"
@@ -38,7 +40,7 @@ func RunDashboardServer(addr string, port int64) (err error) {
 	// view, see dashboard_view.go
 	mux.Handle("/favicon.ico", http.FileServer(assets.FileSystem))
 	mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))
-	mux.HandleFunc("/", viewDashboard)
+	mux.HandleFunc("/", use(viewDashboard, basicAuth))
 
 	address := fmt.Sprintf("%s:%d", addr, port)
 	server := &http.Server{
@@ -58,3 +60,43 @@ func RunDashboardServer(addr string, port int64) (err error) {
 	go server.Serve(ln)
 	return
 }
+
+func use(h http.HandlerFunc, middleware ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
+	for _, m := range middleware {
+		h = m(h)
+	}
+
+	return h
+}
+
+func basicAuth(h http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+
+		w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
+
+		s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
+		if len(s) != 2 {
+			http.Error(w, "Not authorized", 401)
+			return
+		}
+
+		b, err := base64.StdEncoding.DecodeString(s[1])
+		if err != nil {
+			http.Error(w, err.Error(), 401)
+			return
+		}
+
+		pair := strings.SplitN(string(b), ":", 2)
+		if len(pair) != 2 {
+			http.Error(w, "Not authorized", 401)
+			return
+		}
+
+		if pair[0] != DashboardUsername || pair[1] != DashboardPassword {
+			http.Error(w, "Not authorized", 401)
+			return
+		}
+
+		h.ServeHTTP(w, r)
+	}
+}