authentication resolves #3

This commit is contained in:
Josh Baker 2016-03-07 17:37:39 -07:00
parent 4f0d65f184
commit 73ea8b8ee4
13 changed files with 450 additions and 95 deletions

View File

@ -113,6 +113,10 @@ func main() {
if !parseArgs() {
return
}
if len(oneCommand) > 0 && (oneCommand[0] == 'h' || oneCommand[0] == 'H') && strings.Split(strings.ToLower(oneCommand), " ")[0] == "help" {
showHelp()
return
}
addr := fmt.Sprintf("%s:%d", hostname, port)
conn, err := client.Dial(addr)

View File

@ -8,6 +8,7 @@ import (
"os"
"runtime"
"strconv"
"strings"
"github.com/tidwall/tile38/controller"
"github.com/tidwall/tile38/controller/log"
@ -15,23 +16,48 @@ import (
)
var (
dir string
port int
host string
verbose bool
veryVerbose bool
devMode bool
quiet bool
dir string
port int
host string
verbose bool
veryVerbose bool
devMode bool
quiet bool
protectedMode bool = true
)
func main() {
// parse non standard args.
nargs := []string{os.Args[0]}
for i := 1; i < len(os.Args); i++ {
switch os.Args[i] {
case "--protected-mode", "-protected-mode":
i++
if i < len(os.Args) {
switch strings.ToLower(os.Args[i]) {
case "no":
protectedMode = false
case "yes":
protectedMode = true
}
continue
}
fmt.Fprintf(os.Stderr, "protected-mode must be 'yes' or 'no'\n")
os.Exit(1)
case "--dev", "-dev":
devMode = true
continue
}
nargs = append(nargs, os.Args[i])
}
os.Args = nargs
flag.IntVar(&port, "p", 9851, "The listening port.")
flag.StringVar(&host, "h", "127.0.0.1", "The listening host.")
flag.StringVar(&host, "h", "", "The listening host.")
flag.StringVar(&dir, "d", "data", "The data directory.")
flag.BoolVar(&verbose, "v", false, "Enable verbose logging.")
flag.BoolVar(&quiet, "q", false, "Quiet logging. Totally silent.")
flag.BoolVar(&veryVerbose, "vv", false, "Enable very verbose logging.")
flag.BoolVar(&devMode, "dev", false, "Activates dev mode. DEV ONLY.")
flag.Parse()
var logw io.Writer = os.Stderr
if quiet {
@ -43,6 +69,12 @@ func main() {
})
core.DevMode = devMode
core.ShowDebugMessages = veryVerbose
core.ProtectedMode = protectedMode
hostd := ""
if host != "" {
hostd = "Addr: " + host + ", "
}
// _____ _ _ ___ ___
// |_ _|_| |___|_ | . |
@ -53,11 +85,11 @@ func main() {
_______ _______
| | |
|____ | _ | Tile38 %s (%s) %d bit (%s/%s)
| | | Host: %s, Port: %d, PID: %d
| | | %sPort: %d, PID: %d
|____ | _ |
| | | tile38.com
|_______|_______|
`+"\n", core.Version, core.GitSHA, strconv.IntSize, runtime.GOARCH, runtime.GOOS, host, port, os.Getpid())
`+"\n", core.Version, core.GitSHA, strconv.IntSize, runtime.GOARCH, runtime.GOOS, hostd, port, os.Getpid())
if err := controller.ListenAndServe(host, port, dir); err != nil {
log.Fatal(err)

15
controller/auth.go Normal file
View File

@ -0,0 +1,15 @@
package controller
func (c *Controller) cmdAuth(line string) error {
var password string
if line, password = token(line); password == "" {
return errInvalidNumberOfArguments
}
if line != "" {
return errInvalidNumberOfArguments
}
println(password)
return nil
}

162
controller/config.go Normal file
View File

@ -0,0 +1,162 @@
package controller
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"time"
)
// Config is a tile38 config
type Config struct {
FollowHost string `json:"follow_host,omitempty"`
FollowPort int `json:"follow_port,omitempty"`
FollowID string `json:"follow_id,omitempty"`
FollowPos int `json:"follow_pos,omitempty"`
ServerID string `json:"server_id,omitempty"`
ReadOnly bool `json:"read_only,omitempty"`
// Properties
RequirePassP string `json:"requirepass,omitempty"`
RequirePass string `json:"-"`
LeaderAuthP string `json:"leaderauth,omitempty"`
LeaderAuth string `json:"-"`
ProtectedModeP string `json:"protected-mode,omitempty"`
ProtectedMode string `json:"-"`
}
func (c *Controller) loadConfig() error {
data, err := ioutil.ReadFile(c.dir + "/config")
if err != nil {
if os.IsNotExist(err) {
return c.initConfig()
}
return err
}
err = json.Unmarshal(data, &c.config)
if err != nil {
return err
}
// load properties
if err := c.setConfigProperty("requirepass", c.config.RequirePassP, true); err != nil {
return err
}
if err := c.setConfigProperty("leaderauth", c.config.LeaderAuthP, true); err != nil {
return err
}
if err := c.setConfigProperty("protected-mode", c.config.ProtectedModeP, true); err != nil {
return err
}
return nil
}
func (c *Controller) setConfigProperty(name, value string, fromLoad bool) error {
var invalid bool
switch name {
default:
return fmt.Errorf("Unsupported CONFIG parameter: %s", name)
case "requirepass":
c.config.RequirePass = value
case "leaderauth":
c.config.LeaderAuth = value
case "protected-mode":
switch strings.ToLower(value) {
case "":
if fromLoad {
c.config.ProtectedMode = "yes"
} else {
invalid = true
}
case "yes", "no":
c.config.ProtectedMode = strings.ToLower(value)
default:
invalid = true
}
}
if invalid {
return fmt.Errorf("Invalid argument '%s' for CONFIG SET '%s'", value, name)
}
return nil
}
func (c *Controller) getConfigProperty(name string) string {
switch name {
default:
return ""
case "requirepass":
return c.config.RequirePass
case "leaderauth":
return c.config.LeaderAuth
case "protected-mode":
return c.config.ProtectedMode
}
}
func (c *Controller) initConfig() error {
c.config = Config{ServerID: randomKey(16)}
return c.writeConfig(true)
}
func (c *Controller) writeConfig(writeProperties bool) error {
var err error
bak := c.config
defer func() {
if err != nil {
// revert changes
c.config = bak
}
}()
if writeProperties {
// save properties
c.config.RequirePassP = c.config.RequirePass
c.config.LeaderAuthP = c.config.LeaderAuth
c.config.ProtectedModeP = c.config.ProtectedMode
}
var data []byte
data, err = json.MarshalIndent(c.config, "", "\t")
if err != nil {
return err
}
err = ioutil.WriteFile(c.dir+"/config", data, 0600)
if err != nil {
return err
}
return nil
}
func (c *Controller) cmdConfig(line string) (string, error) {
var start = time.Now()
var cmd, name, value string
if line, cmd = token(line); cmd == "" {
return "", errInvalidNumberOfArguments
}
var buf bytes.Buffer
buf.WriteString(`{"ok":true`)
switch strings.ToLower(cmd) {
default:
return "", errInvalidArgument(cmd)
case "get":
if line, name = token(line); name == "" || line != "" {
return "", errInvalidNumberOfArguments
}
value = c.getConfigProperty(name)
buf.WriteString(`,"value":` + jsonString(value))
case "set":
if line, name = token(line); name == "" {
return "", errInvalidNumberOfArguments
}
value = strings.TrimSpace(line)
if err := c.setConfigProperty(name, value, false); err != nil {
return "", err
}
case "rewrite":
if err := c.writeConfig(true); err != nil {
return "", err
}
}
buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}")
return buf.String(), nil
}

View File

@ -4,14 +4,12 @@ import (
"bufio"
"bytes"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"runtime"
"strings"
"sync"
"time"
@ -64,16 +62,6 @@ type Controller struct {
shrinking bool // aof shrinking flag
}
// Config is a tile38 config
type Config struct {
FollowHost string `json:"follow_host,omitempty"`
FollowPort int `json:"follow_port,omitempty"`
FollowID string `json:"follow_id,omitempty"`
FollowPos int `json:"follow_pos,omitempty"`
ServerID string `json:"server_id,omitempty"`
ReadOnly bool `json:"read_only,omitempty"`
}
// ListenAndServe starts a new tile38 server
func ListenAndServe(host string, port int, dir string) error {
log.Infof("Server started, Tile38 version %s, git %s", core.Version, core.GitSHA)
@ -113,8 +101,8 @@ func ListenAndServe(host string, port int, dir string) error {
c.mu.Unlock()
}()
go c.processLives()
handler := func(command []byte, conn net.Conn, rd *bufio.Reader, w io.Writer, websocket bool) error {
err := c.handleInputCommand(string(command), w)
handler := func(conn *server.Conn, command []byte, rd *bufio.Reader, w io.Writer, websocket bool) error {
err := c.handleInputCommand(conn, string(command), w)
if err != nil {
if err.Error() == "going live" {
return c.goLive(err, conn, rd, websocket)
@ -123,7 +111,21 @@ func ListenAndServe(host string, port int, dir string) error {
}
return nil
}
return server.ListenAndServe(host, port, handler)
protected := func() bool {
if !core.ProtectedMode {
// --protected-mode no
return false
}
if host != "" && host != "127.0.0.1" && host != "::1" && host != "localhost" {
// -h address
return false
}
c.mu.RLock()
is := c.config.ProtectedMode != "no" && c.config.RequirePass == ""
c.mu.RUnlock()
return is
}
return server.ListenAndServe(host, port, protected, handler)
}
func (c *Controller) setCol(key string, col *collection.Collection) {
@ -156,12 +158,12 @@ func isReservedFieldName(field string) bool {
return false
}
func (c *Controller) handleInputCommand(line string, w io.Writer) error {
func (c *Controller) handleInputCommand(conn *server.Conn, line string, w io.Writer) error {
if core.ShowDebugMessages && line != "pInG" {
log.Debug(line)
}
start := time.Now()
// Ping and Help. Just send back the response. No need to put through the pipeline.
// Ping. Just send back the response. No need to put through the pipeline.
if len(line) == 4 && (line[0] == 'p' || line[0] == 'P') && lc(line, "ping") {
w.Write([]byte(`{"ok":true,"ping":"pong","elapsed":"` + time.Now().Sub(start).String() + `"}`))
return nil
@ -180,6 +182,26 @@ func (c *Controller) handleInputCommand(line string, w io.Writer) error {
if cmd == "" {
return writeErr(errors.New("empty command"))
}
if !conn.Authenticated {
c.mu.RLock()
requirePass := c.config.RequirePass
c.mu.RUnlock()
if requirePass != "" {
// This better be an AUTH command.
if cmd != "auth" {
// Just shut down the pipeline now. The less the client connection knows the better.
return writeErr(errors.New("authentication required"))
}
password, _ := token(line)
if requirePass == strings.TrimSpace(password) {
conn.Authenticated = true
} else {
return writeErr(errors.New("invalid password"))
}
}
}
// choose the locking strategy
switch cmd {
default:
@ -203,7 +225,7 @@ func (c *Controller) handleInputCommand(line string, w io.Writer) error {
if c.config.FollowHost != "" && !c.fcup {
return writeErr(errors.New("catching up to leader"))
}
case "follow", "readonly":
case "follow", "readonly", "config":
// system operations
// does not write to aof, but requires a write lock.
c.mu.Lock()
@ -235,37 +257,6 @@ func (c *Controller) handleInputCommand(line string, w io.Writer) error {
return nil
}
func (c *Controller) loadConfig() error {
data, err := ioutil.ReadFile(c.dir + "/config")
if err != nil {
if os.IsNotExist(err) {
return c.initConfig()
}
return err
}
err = json.Unmarshal(data, &c.config)
if err != nil {
return err
}
return nil
}
func (c *Controller) initConfig() error {
c.config = Config{ServerID: randomKey(16)}
return c.writeConfig()
}
func (c *Controller) writeConfig() error {
data, err := json.MarshalIndent(c.config, "", "\t")
if err != nil {
return err
}
if err := ioutil.WriteFile(c.dir+"/config", data, 0600); err != nil {
return err
}
return nil
}
func randomKey(n int) string {
b := make([]byte, n)
nn, err := rand.Read(b)
@ -322,9 +313,14 @@ func (c *Controller) command(line string, w io.Writer) (resp string, d commandDe
case "follow":
err = c.cmdFollow(nline)
resp = okResp()
case "config":
resp, err = c.cmdConfig(nline)
case "readonly":
err = c.cmdReadOnly(nline)
resp = okResp()
case "auth":
err = c.cmdAuth(nline)
resp = okResp()
case "stats":
resp, err = c.cmdServer(nline)
case "server":

View File

@ -71,7 +71,7 @@ func (c *Controller) cmdFollow(line string) error {
c.config.FollowHost = host
c.config.FollowPort = port
}
if err := c.writeConfig(); err != nil {
if err := c.writeConfig(false); err != nil {
c.config = pconfig // revert
return err
}

View File

@ -31,7 +31,7 @@ func (c *Controller) cmdReadOnly(line string) error {
c.config.ReadOnly = false
log.Info("read write")
}
err := c.writeConfig()
err := c.writeConfig(false)
if err != nil {
c.config = backup
return err

View File

@ -14,12 +14,44 @@ import (
"github.com/tidwall/tile38/core"
)
// This phrase is copied nearly verbatim from Redis.
// https://github.com/antirez/redis/blob/cf42c48adcea05c1bd4b939fcd36a01f23ec6303/src/networking.c
var deniedMessage = []byte(strings.TrimSpace(`
ACCESS DENIED
Tile38 is running in protected mode because protected mode is enabled, no host
address was specified, no authentication password is requested to clients. In
this mode connections are only accepted from the loopback interface. If you
want to connect from external computers to Tile38 you may adopt one of the
following solutions:
1) Disable protected mode sending the command 'CONFIG SET protected-mode no'
from the loopback interface by connecting to Tile38 from the same host
the server is running, however MAKE SURE Tile38 is not publicly accessible
from internet if you do so. Use CONFIG REWRITE to make this change
permanent.
2) Alternatively you can just disable the protected mode by editing the Tile38
configuration file, and setting the protected mode option to 'no', and then
restarting the server.
3) If you started the server manually just for testing, restart it with the
'--protected-mode no' option.
4) Setup a host address or an authentication password.
NOTE: You only need to do one of the above things in order for the server
to start accepting connections from the outside.
`) + "\r\n")
type Conn struct {
net.Conn
Authenticated bool
}
var errCloseHTTP = errors.New("close http")
// ListenAndServe starts a tile38 server at the specified address.
func ListenAndServe(
host string, port int,
handler func(command []byte, conn net.Conn, rd *bufio.Reader, w io.Writer, websocket bool) error,
protected func() bool,
handler func(conn *Conn, command []byte, rd *bufio.Reader, w io.Writer, websocket bool) error,
) error {
ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
@ -32,13 +64,14 @@ func ListenAndServe(
log.Error(err)
continue
}
go handleConn(conn, handler)
go handleConn(&Conn{Conn: conn}, protected, handler)
}
}
func handleConn(
conn net.Conn,
handler func(command []byte, conn net.Conn, rd *bufio.Reader, w io.Writer, websocket bool) error,
conn *Conn,
protected func() bool,
handler func(conn *Conn, command []byte, rd *bufio.Reader, w io.Writer, websocket bool) error,
) {
if core.ShowDebugMessages {
addr := conn.RemoteAddr().String()
@ -46,6 +79,14 @@ func handleConn(
defer func() {
log.Debugf("closed connection: %s", addr)
}()
if !strings.HasPrefix(addr, "127.0.0.1:") && !strings.HasPrefix(addr, "[::1]:") {
if protected() {
// This is a protected server. Only loopback is allowed.
conn.Write(deniedMessage)
conn.Close()
return
}
}
}
defer conn.Close()
rd := bufio.NewReader(conn)
@ -59,7 +100,8 @@ func handleConn(
return io.EOF
}
var b bytes.Buffer
if err := handler(command, conn, rd, &b, proto == client.WebSocket); err != nil {
if err := handler(conn, command, rd, &b, proto == client.WebSocket); err != nil {
if proto == client.HTTP {
conn.Write([]byte(`HTTP/1.1 500 ` + err.Error() + "\r\nConnection: close\r\n\r\n"))
}

View File

@ -33,13 +33,13 @@ func (c Command) String() string {
func (c Command) TermOutput(indent string) string {
line1 := bright + strings.Replace(c.String(), " ", " "+clear+gray, 1) + clear
line2 := yellow + "summary: " + clear + c.Summary
line3 := yellow + "since: " + clear + c.Since
return indent + line1 + "\n" + indent + line2 + "\n" + indent + line3 + "\n"
//line3 := yellow + "since: " + clear + c.Since
return indent + line1 + "\n" + indent + line2 + "\n" //+ indent + line3 + "\n"
}
type EnumArg struct {
Name string `json:"name"`
Arguments []Argument `json:"arguments`
Arguments []Argument `json:"arguments"`
}
func (a EnumArg) String() string {
@ -846,12 +846,43 @@ var commandsJSON = `{
"since": "1.0.0",
"group": "search"
},
"PING": {
"summary":"Ping the server",
"complexity": "O(1)",
"arguments": [],
"since": "1.0.0",
"group": "server"
"CONFIG GET": {
"summary": "Authenticate to the server",
"arguments": [
{
"name": "which",
"enumargs": [
{
"name": "GET",
"arguments":[
{
"name": "parameter",
"type": "string"
}
]
},
{
"name": "SET",
"arguments":[
{
"name": "parameter",
"type": "string"
},
{
"name": "value",
"type": "string",
"optional": true
}
]
},
{
"name": "REWRITE",
"arguments":[]
}
]
}
],
"group": "connection"
},
"SERVER": {
"summary":"Show server stats and details",
@ -931,10 +962,25 @@ var commandsJSON = `{
},
"AOFSHRINK": {
"summary": "Shrinks the aof in the background",
"complexity": "O(1)",
"arguments": [],
"since": "1.0.0",
"group": "replication"
},
"PING": {
"summary": "Ping the server",
"group": "connection"
},
"QUIT": {
"summary": "Close the connection",
"group": "connection"
},
"AUTH": {
"summary": "Authenticate to the server",
"arguments": [
{
"name": "password",
"type": "string"
}
],
"group": "connection"
}
}`
@ -960,6 +1006,8 @@ var commandsJSON = `{
// HELP -- Prints this menu. -- O(1) -- F()
// READONLY value -- Turn on or off readonly mode. -- O(1) -- F(value boolean)
// FLUSHDB -- Removes all keys. -- O(1) -- F()
// CONFIG SET property value -- Set a config property. Is not yet permanent.
// CONFIG REWRITE -- Make config changes permanent.
// --- Replication ---
// FOLLOW host port -- Follows a leader host. -- O(1) F(host string, port integer)
@ -990,3 +1038,6 @@ var commandsJSON = `{
// QUADKEY... QUADKEY key -- Quadkey. -- F(key quadkey)
// TILE... TILE x y z -- Google XYZ tile. -- F(x double, y double, z double)
// GET... GET key id -- An internal object. -- F(key string, id string)
// --- Security ---
// AUTH password -- Authenticate to server.

View File

@ -701,12 +701,43 @@
"since": "1.0.0",
"group": "search"
},
"PING": {
"summary":"Ping the server",
"complexity": "O(1)",
"arguments": [],
"since": "1.0.0",
"group": "server"
"CONFIG": {
"summary": "Authenticate to the server",
"arguments": [
{
"name": "which",
"enumargs": [
{
"name": "GET",
"arguments":[
{
"name": "parameter",
"type": "string"
}
]
},
{
"name": "SET",
"arguments":[
{
"name": "parameter",
"type": "string"
},
{
"name": "value",
"type": "string",
"optional": true
}
]
},
{
"name": "REWRITE",
"arguments":[]
}
]
}
],
"group": "connection"
},
"SERVER": {
"summary":"Show server stats and details",
@ -786,9 +817,24 @@
},
"AOFSHRINK": {
"summary": "Shrinks the aof in the background",
"complexity": "O(1)",
"arguments": [],
"since": "1.0.0",
"group": "replication"
},
"PING": {
"summary": "Ping the server",
"group": "connection"
},
"QUIT": {
"summary": "Close the connection",
"group": "connection"
},
"AUTH": {
"summary": "Authenticate to the server",
"arguments": [
{
"name": "password",
"type": "string"
}
],
"group": "connection"
}
}

View File

@ -35,13 +35,13 @@ func (c Command) String() string {
func (c Command) TermOutput(indent string) string {
line1 := bright + strings.Replace(c.String(), " ", " "+clear+gray, 1) + clear
line2 := yellow + "summary: " + clear + c.Summary
line3 := yellow + "since: " + clear + c.Since
return indent + line1 + "\n" + indent + line2 + "\n" + indent + line3 + "\n"
//line3 := yellow + "since: " + clear + c.Since
return indent + line1 + "\n" + indent + line2 + "\n" //+ indent + line3 + "\n"
}
type EnumArg struct {
Name string `json:"name"`
Arguments []Argument `json:"arguments`
Arguments []Argument `json:"arguments"`
}
func (a EnumArg) String() string {
@ -169,6 +169,8 @@ var commandsJSON = `{{.CommandsJSON}}`
// HELP -- Prints this menu. -- O(1) -- F()
// READONLY value -- Turn on or off readonly mode. -- O(1) -- F(value boolean)
// FLUSHDB -- Removes all keys. -- O(1) -- F()
// CONFIG SET property value -- Set a config property. Is not yet permanent.
// CONFIG REWRITE -- Make config changes permanent.
// --- Replication ---
// FOLLOW host port -- Follows a leader host. -- O(1) F(host string, port integer)
@ -199,3 +201,6 @@ var commandsJSON = `{{.CommandsJSON}}`
// QUADKEY... QUADKEY key -- Quadkey. -- F(key quadkey)
// TILE... TILE x y z -- Google XYZ tile. -- F(x double, y double, z double)
// GET... GET key id -- An internal object. -- F(key string, id string)
// --- Security ---
// AUTH password -- Authenticate to server.

View File

@ -5,3 +5,5 @@ var DevMode = false
// ShowDebugMessages allows for log.Debug to print to console.
var ShowDebugMessages = false
var ProtectedMode = true

View File

@ -1,7 +1,7 @@
package core
var (
Version string
BuildTime string
GitSHA string
Version = "0.0.0"
BuildTime = ""
GitSHA = "0000000"
)