mirror of https://github.com/portainer/portainer
refactor(api): API overhaul (#392)
parent
d9f6124609
commit
0a38bba874
|
@ -2,3 +2,4 @@ node_modules
|
||||||
bower_components
|
bower_components
|
||||||
dist
|
dist
|
||||||
portainer-checksum.txt
|
portainer-checksum.txt
|
||||||
|
api/cmd/portainer/portainer-*
|
||||||
|
|
102
api/api.go
102
api/api.go
|
@ -1,102 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"github.com/gorilla/securecookie"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
api struct {
|
|
||||||
endpoint *url.URL
|
|
||||||
bindAddress string
|
|
||||||
assetPath string
|
|
||||||
dataPath string
|
|
||||||
tlsConfig *tls.Config
|
|
||||||
templatesURL string
|
|
||||||
dataStore *dataStore
|
|
||||||
secret []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
apiConfig struct {
|
|
||||||
Endpoint string
|
|
||||||
BindAddress string
|
|
||||||
AssetPath string
|
|
||||||
DataPath string
|
|
||||||
SwarmSupport bool
|
|
||||||
TLSEnabled bool
|
|
||||||
TLSCACertPath string
|
|
||||||
TLSCertPath string
|
|
||||||
TLSKeyPath string
|
|
||||||
TemplatesURL string
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
datastoreFileName = "portainer.db"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errSecretKeyGeneration = errors.New("Unable to generate secret key to sign JWT")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *api) run(settings *Settings) {
|
|
||||||
err := a.initDatabase()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer a.cleanUp()
|
|
||||||
|
|
||||||
handler := a.newHandler(settings)
|
|
||||||
log.Printf("Starting portainer on %s", a.bindAddress)
|
|
||||||
if err := http.ListenAndServe(a.bindAddress, handler); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *api) cleanUp() {
|
|
||||||
a.dataStore.cleanUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *api) initDatabase() error {
|
|
||||||
dataStore, err := newDataStore(a.dataPath + "/" + datastoreFileName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = dataStore.initDataStore()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
a.dataStore = dataStore
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAPI(apiConfig apiConfig) *api {
|
|
||||||
endpointURL, err := url.Parse(apiConfig.Endpoint)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
secret := securecookie.GenerateRandomKey(32)
|
|
||||||
if secret == nil {
|
|
||||||
log.Fatal(errSecretKeyGeneration)
|
|
||||||
}
|
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
|
||||||
if apiConfig.TLSEnabled {
|
|
||||||
tlsConfig = newTLSConfig(apiConfig.TLSCACertPath, apiConfig.TLSCertPath, apiConfig.TLSKeyPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &api{
|
|
||||||
endpoint: endpointURL,
|
|
||||||
bindAddress: apiConfig.BindAddress,
|
|
||||||
assetPath: apiConfig.AssetPath,
|
|
||||||
dataPath: apiConfig.DataPath,
|
|
||||||
tlsConfig: tlsConfig,
|
|
||||||
templatesURL: apiConfig.TemplatesURL,
|
|
||||||
secret: secret,
|
|
||||||
}
|
|
||||||
}
|
|
88
api/auth.go
88
api/auth.go
|
@ -1,88 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/asaskevich/govalidator"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
credentials struct {
|
|
||||||
Username string `valid:"alphanum,required"`
|
|
||||||
Password string `valid:"length(8)"`
|
|
||||||
}
|
|
||||||
authResponse struct {
|
|
||||||
JWT string `json:"jwt"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func hashPassword(password string) (string, error) {
|
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
return string(hash), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkPasswordValidity(password string, hash string) error {
|
|
||||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
|
||||||
}
|
|
||||||
|
|
||||||
// authHandler defines a handler function used to authenticate users
|
|
||||||
func (api *api) authHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" {
|
|
||||||
w.Header().Set("Allow", "POST")
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var credentials credentials
|
|
||||||
err = json.Unmarshal(body, &credentials)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to parse credentials", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = govalidator.ValidateStruct(credentials)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid credentials format", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var username = credentials.Username
|
|
||||||
var password = credentials.Password
|
|
||||||
u, err := api.dataStore.getUserByUsername(username)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("User not found: %s", username)
|
|
||||||
http.Error(w, "User not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = checkPasswordValidity(password, u.Password)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Invalid credentials for user: %s", username)
|
|
||||||
http.Error(w, "Invalid credentials", http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := api.generateJWTToken(username)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Unable to generate JWT token: %s", err.Error())
|
|
||||||
http.Error(w, "Unable to generate JWT token", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response := authResponse{
|
|
||||||
JWT: token,
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(response)
|
|
||||||
}
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store defines the implementation of portainer.DataStore using
|
||||||
|
// BoltDB as the storage system.
|
||||||
|
type Store struct {
|
||||||
|
// Path where is stored the BoltDB database.
|
||||||
|
Path string
|
||||||
|
|
||||||
|
// Services
|
||||||
|
UserService *UserService
|
||||||
|
|
||||||
|
db *bolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
databaseFileName = "portainer.db"
|
||||||
|
userBucketName = "users"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewStore initializes a new Store and the associated services
|
||||||
|
func NewStore(storePath string) *Store {
|
||||||
|
store := &Store{
|
||||||
|
Path: storePath,
|
||||||
|
UserService: &UserService{},
|
||||||
|
}
|
||||||
|
store.UserService.store = store
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens and initializes the BoltDB database.
|
||||||
|
func (store *Store) Open() error {
|
||||||
|
path := store.Path + "/" + databaseFileName
|
||||||
|
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
store.db = db
|
||||||
|
return db.Update(func(tx *bolt.Tx) error {
|
||||||
|
_, err := tx.CreateBucketIfNotExists([]byte(userBucketName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the BoltDB database.
|
||||||
|
func (store *Store) Close() error {
|
||||||
|
if store.db != nil {
|
||||||
|
return store.db.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarshalUser encodes a user to binary format.
|
||||||
|
func MarshalUser(user *portainer.User) ([]byte, error) {
|
||||||
|
return json.Marshal(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalUser decodes a user from a binary data.
|
||||||
|
func UnmarshalUser(data []byte, user *portainer.User) error {
|
||||||
|
return json.Unmarshal(data, user)
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
"github.com/portainer/portainer/bolt/internal"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserService represents a service for managing users.
|
||||||
|
type UserService struct {
|
||||||
|
store *Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// User returns a user by username.
|
||||||
|
func (service *UserService) User(username string) (*portainer.User, error) {
|
||||||
|
var data []byte
|
||||||
|
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(userBucketName))
|
||||||
|
value := bucket.Get([]byte(username))
|
||||||
|
if value == nil {
|
||||||
|
return portainer.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
data = make([]byte, len(value))
|
||||||
|
copy(data, value)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var user portainer.User
|
||||||
|
err = internal.UnmarshalUser(data, &user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser saves a user.
|
||||||
|
func (service *UserService) UpdateUser(user *portainer.User) error {
|
||||||
|
data, err := internal.MarshalUser(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(userBucketName))
|
||||||
|
err = bucket.Put([]byte(user.Username), data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service implements the CLIService interface
|
||||||
|
type Service struct{}
|
||||||
|
|
||||||
|
const (
|
||||||
|
errInvalidEnpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
|
||||||
|
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseFlags parse the CLI flags and return a portainer.Flags struct
|
||||||
|
func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||||
|
kingpin.Version(version)
|
||||||
|
|
||||||
|
flags := &portainer.CLIFlags{
|
||||||
|
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String(),
|
||||||
|
Assets: kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String(),
|
||||||
|
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default("/data").Short('d').String(),
|
||||||
|
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String(),
|
||||||
|
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||||
|
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||||
|
Swarm: kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool(),
|
||||||
|
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String(),
|
||||||
|
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default("false").Bool(),
|
||||||
|
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String(),
|
||||||
|
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String(),
|
||||||
|
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
kingpin.Parse()
|
||||||
|
return flags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateFlags validates the values of the flags.
|
||||||
|
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||||
|
if !strings.HasPrefix(*flags.Endpoint, "unix://") && !strings.HasPrefix(*flags.Endpoint, "tcp://") {
|
||||||
|
return errInvalidEnpointProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(*flags.Endpoint, "unix://") {
|
||||||
|
socketPath := strings.TrimPrefix(*flags.Endpoint, "unix://")
|
||||||
|
if _, err := os.Stat(socketPath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return errSocketNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,46 +1,40 @@
|
||||||
package main
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// pair defines a key/value pair
|
type pairList []portainer.Pair
|
||||||
type pair struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// pairList defines an array of Label
|
// Set implementation for a list of portainer.Pair
|
||||||
type pairList []pair
|
|
||||||
|
|
||||||
// Set implementation for Labels
|
|
||||||
func (l *pairList) Set(value string) error {
|
func (l *pairList) Set(value string) error {
|
||||||
parts := strings.SplitN(value, "=", 2)
|
parts := strings.SplitN(value, "=", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return fmt.Errorf("expected NAME=VALUE got '%s'", value)
|
return fmt.Errorf("expected NAME=VALUE got '%s'", value)
|
||||||
}
|
}
|
||||||
p := new(pair)
|
p := new(portainer.Pair)
|
||||||
p.Name = parts[0]
|
p.Name = parts[0]
|
||||||
p.Value = parts[1]
|
p.Value = parts[1]
|
||||||
*l = append(*l, *p)
|
*l = append(*l, *p)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implementation for Labels
|
// String implementation for a list of pair
|
||||||
func (l *pairList) String() string {
|
func (l *pairList) String() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsCumulative implementation for Labels
|
// IsCumulative implementation for a list of pair
|
||||||
func (l *pairList) IsCumulative() bool {
|
func (l *pairList) IsCumulative() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// LabelParser defines a custom parser for Labels flags
|
func pairs(s kingpin.Settings) (target *[]portainer.Pair) {
|
||||||
func pairs(s kingpin.Settings) (target *[]pair) {
|
target = new([]portainer.Pair)
|
||||||
target = new([]pair)
|
|
||||||
s.SetValue((*pairList)(target))
|
s.SetValue((*pairList)(target))
|
||||||
return
|
return
|
||||||
}
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package main // import "github.com/portainer/portainer"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
"github.com/portainer/portainer/bolt"
|
||||||
|
"github.com/portainer/portainer/cli"
|
||||||
|
"github.com/portainer/portainer/crypto"
|
||||||
|
"github.com/portainer/portainer/http"
|
||||||
|
"github.com/portainer/portainer/jwt"
|
||||||
|
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var cli portainer.CLIService = &cli.Service{}
|
||||||
|
flags, err := cli.ParseFlags(portainer.APIVersion)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.ValidateFlags(flags)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := &portainer.Settings{
|
||||||
|
Swarm: *flags.Swarm,
|
||||||
|
HiddenLabels: *flags.Labels,
|
||||||
|
Logo: *flags.Logo,
|
||||||
|
}
|
||||||
|
|
||||||
|
var store = bolt.NewStore(*flags.Data)
|
||||||
|
err = store.Open()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
jwtService, err := jwt.NewService()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cryptoService portainer.CryptoService = &crypto.Service{}
|
||||||
|
|
||||||
|
endpointConfiguration := &portainer.EndpointConfiguration{
|
||||||
|
Endpoint: *flags.Endpoint,
|
||||||
|
TLS: *flags.TLSVerify,
|
||||||
|
TLSCACertPath: *flags.TLSCacert,
|
||||||
|
TLSCertPath: *flags.TLSCert,
|
||||||
|
TLSKeyPath: *flags.TLSKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
var server portainer.Server = &http.Server{
|
||||||
|
BindAddress: *flags.Addr,
|
||||||
|
AssetsPath: *flags.Assets,
|
||||||
|
Settings: settings,
|
||||||
|
TemplatesURL: *flags.Templates,
|
||||||
|
UserService: store.UserService,
|
||||||
|
CryptoService: cryptoService,
|
||||||
|
JWTService: jwtService,
|
||||||
|
EndpointConfig: endpointConfiguration,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Starting Portainer on %s", *flags.Addr)
|
||||||
|
err = server.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service represents a service for encrypting/hashing data.
|
||||||
|
type Service struct{}
|
||||||
|
|
||||||
|
// Hash hashes a string using the bcrypt algorithm
|
||||||
|
func (*Service) Hash(data string) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareHashAndData compares a hash to clear data and returns an error if the comparison fails.
|
||||||
|
func (*Service) CompareHashAndData(hash string, data string) error {
|
||||||
|
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(data))
|
||||||
|
}
|
|
@ -1,98 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"github.com/boltdb/bolt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
userBucketName = "users"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
dataStore struct {
|
|
||||||
db *bolt.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
userItem struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password,omitempty"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errUserNotFound = errors.New("User not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (dataStore *dataStore) initDataStore() error {
|
|
||||||
return dataStore.db.Update(func(tx *bolt.Tx) error {
|
|
||||||
_, err := tx.CreateBucketIfNotExists([]byte(userBucketName))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dataStore *dataStore) cleanUp() {
|
|
||||||
dataStore.db.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDataStore(databasePath string) (*dataStore, error) {
|
|
||||||
db, err := bolt.Open(databasePath, 0600, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dataStore{
|
|
||||||
db: db,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dataStore *dataStore) getUserByUsername(username string) (*userItem, error) {
|
|
||||||
var data []byte
|
|
||||||
|
|
||||||
err := dataStore.db.View(func(tx *bolt.Tx) error {
|
|
||||||
bucket := tx.Bucket([]byte(userBucketName))
|
|
||||||
value := bucket.Get([]byte(username))
|
|
||||||
if value == nil {
|
|
||||||
return errUserNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
data = make([]byte, len(value))
|
|
||||||
copy(data, value)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var user userItem
|
|
||||||
err = json.Unmarshal(data, &user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dataStore *dataStore) updateUser(user userItem) error {
|
|
||||||
buffer, err := json.Marshal(user)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = dataStore.db.Update(func(tx *bolt.Tx) error {
|
|
||||||
bucket := tx.Bucket([]byte(userBucketName))
|
|
||||||
err = bucket.Put([]byte(user.Username), buffer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package portainer
|
||||||
|
|
||||||
|
// General errors.
|
||||||
|
const (
|
||||||
|
ErrUnauthorized = Error("Unauthorized")
|
||||||
|
)
|
||||||
|
|
||||||
|
// User errors.
|
||||||
|
const (
|
||||||
|
ErrUserNotFound = Error("User not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Crypto errors.
|
||||||
|
const (
|
||||||
|
ErrCryptoHashFailure = Error("Unable to hash data")
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWT errors.
|
||||||
|
const (
|
||||||
|
ErrSecretGeneration = Error("Unable to generate secret key")
|
||||||
|
ErrInvalidJWTToken = Error("Invalid JWT token")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error represents an application error.
|
||||||
|
type Error string
|
||||||
|
|
||||||
|
// Error returns the error message.
|
||||||
|
func (e Error) Error() string { return string(e) }
|
24
api/exec.go
24
api/exec.go
|
@ -1,24 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/net/websocket"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// execContainer is used to create a websocket communication with an exec instance
|
|
||||||
func (a *api) execContainer(ws *websocket.Conn) {
|
|
||||||
qry := ws.Request().URL.Query()
|
|
||||||
execID := qry.Get("id")
|
|
||||||
|
|
||||||
var host string
|
|
||||||
if a.endpoint.Scheme == "tcp" {
|
|
||||||
host = a.endpoint.Host
|
|
||||||
} else if a.endpoint.Scheme == "unix" {
|
|
||||||
host = a.endpoint.Path
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := hijack(host, a.endpoint.Scheme, "POST", "/exec/"+execID+"/start", a.tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
|
|
||||||
log.Fatalf("error during hijack: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"golang.org/x/net/websocket"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// newHandler creates a new http.Handler
|
|
||||||
func (a *api) newHandler(settings *Settings) http.Handler {
|
|
||||||
var (
|
|
||||||
mux = mux.NewRouter()
|
|
||||||
fileHandler = http.FileServer(http.Dir(a.assetPath))
|
|
||||||
)
|
|
||||||
handler := a.newAPIHandler()
|
|
||||||
|
|
||||||
mux.Handle("/ws/exec", websocket.Handler(a.execContainer))
|
|
||||||
mux.HandleFunc("/auth", a.authHandler)
|
|
||||||
mux.Handle("/users", addMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
a.usersHandler(w, r)
|
|
||||||
}), a.authenticate, secureHeaders))
|
|
||||||
mux.Handle("/users/{username}", addMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
a.userHandler(w, r)
|
|
||||||
}), a.authenticate, secureHeaders))
|
|
||||||
mux.Handle("/users/{username}/passwd", addMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
a.userPasswordHandler(w, r)
|
|
||||||
}), a.authenticate, secureHeaders))
|
|
||||||
mux.HandleFunc("/users/admin/check", a.checkAdminHandler)
|
|
||||||
mux.HandleFunc("/users/admin/init", a.initAdminHandler)
|
|
||||||
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
settingsHandler(w, r, settings)
|
|
||||||
})
|
|
||||||
mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
templatesHandler(w, r, a.templatesURL)
|
|
||||||
})
|
|
||||||
mux.PathPrefix("/dockerapi/").Handler(http.StripPrefix("/dockerapi", addMiddleware(handler, a.authenticate, secureHeaders)))
|
|
||||||
mux.PathPrefix("/").Handler(http.StripPrefix("/", fileHandler))
|
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
mux.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// newAPIHandler initializes a new http.Handler based on the URL scheme
|
|
||||||
func (a *api) newAPIHandler() http.Handler {
|
|
||||||
var handler http.Handler
|
|
||||||
var endpoint = *a.endpoint
|
|
||||||
if endpoint.Scheme == "tcp" {
|
|
||||||
if a.tlsConfig != nil {
|
|
||||||
handler = a.newTCPHandlerWithTLS(&endpoint)
|
|
||||||
} else {
|
|
||||||
handler = a.newTCPHandler(&endpoint)
|
|
||||||
}
|
|
||||||
} else if endpoint.Scheme == "unix" {
|
|
||||||
socketPath := endpoint.Path
|
|
||||||
if _, err := os.Stat(socketPath); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
log.Fatalf("Unix socket %s does not exist", socketPath)
|
|
||||||
}
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
handler = a.newUnixHandler(socketPath)
|
|
||||||
} else {
|
|
||||||
log.Fatalf("Bad Docker enpoint: %v. Only unix:// and tcp:// are supported.", &endpoint)
|
|
||||||
}
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// newUnixHandler initializes a new UnixHandler
|
|
||||||
func (a *api) newUnixHandler(e string) http.Handler {
|
|
||||||
return &unixHandler{e}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTCPHandler initializes a HTTP reverse proxy
|
|
||||||
func (a *api) newTCPHandler(u *url.URL) http.Handler {
|
|
||||||
u.Scheme = "http"
|
|
||||||
return httputil.NewSingleHostReverseProxy(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTCPHandlerWithL initializes a HTTPS reverse proxy with a TLS configuration
|
|
||||||
func (a *api) newTCPHandlerWithTLS(u *url.URL) http.Handler {
|
|
||||||
u.Scheme = "https"
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(u)
|
|
||||||
proxy.Transport = &http.Transport{
|
|
||||||
TLSClientConfig: a.tlsConfig,
|
|
||||||
}
|
|
||||||
return proxy
|
|
||||||
}
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthHandler represents an HTTP API handler for managing authentication.
|
||||||
|
type AuthHandler struct {
|
||||||
|
*mux.Router
|
||||||
|
Logger *log.Logger
|
||||||
|
UserService portainer.UserService
|
||||||
|
CryptoService portainer.CryptoService
|
||||||
|
JWTService portainer.JWTService
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ErrInvalidCredentialsFormat is an error raised when credentials format is not valid
|
||||||
|
ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format")
|
||||||
|
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
|
||||||
|
ErrInvalidCredentials = portainer.Error("Invalid credentials")
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewAuthHandler returns a new instance of DialHandler.
|
||||||
|
func NewAuthHandler() *AuthHandler {
|
||||||
|
h := &AuthHandler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
|
}
|
||||||
|
h.HandleFunc("/auth", h.handlePostAuth)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
handleNotAllowed(w, []string{"POST"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req postAuthRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := govalidator.ValidateStruct(req)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var username = req.Username
|
||||||
|
var password = req.Password
|
||||||
|
|
||||||
|
u, err := handler.UserService.User(username)
|
||||||
|
if err == portainer.ErrUserNotFound {
|
||||||
|
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenData := &portainer.TokenData{
|
||||||
|
username,
|
||||||
|
}
|
||||||
|
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
type postAuthRequest struct {
|
||||||
|
Username string `valid:"alphanum,required"`
|
||||||
|
Password string `valid:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type postAuthResponse struct {
|
||||||
|
JWT string `json:"jwt"`
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
|
||||||
|
type DockerHandler struct {
|
||||||
|
*mux.Router
|
||||||
|
Logger *log.Logger
|
||||||
|
middleWareService *middleWareService
|
||||||
|
proxy http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDockerHandler returns a new instance of DockerHandler.
|
||||||
|
func NewDockerHandler(middleWareService *middleWareService) *DockerHandler {
|
||||||
|
h := &DockerHandler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
|
middleWareService: middleWareService,
|
||||||
|
}
|
||||||
|
h.PathPrefix("/").Handler(middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.proxyRequestsToDockerAPI(w, r)
|
||||||
|
})))
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handler.proxy.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *DockerHandler) setupProxy(config *portainer.EndpointConfiguration) error {
|
||||||
|
var proxy http.Handler
|
||||||
|
endpointURL, err := url.Parse(config.Endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if endpointURL.Scheme == "tcp" {
|
||||||
|
if config.TLS {
|
||||||
|
proxy, err = newHTTPSProxy(endpointURL, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
proxy = newHTTPProxy(endpointURL)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Assume unix:// scheme
|
||||||
|
proxy = newSocketProxy(endpointURL.Path)
|
||||||
|
}
|
||||||
|
handler.proxy = proxy
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPProxy(u *url.URL) http.Handler {
|
||||||
|
u.Scheme = "http"
|
||||||
|
return httputil.NewSingleHostReverseProxy(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPSProxy(u *url.URL, endpointConfig *portainer.EndpointConfiguration) (http.Handler, error) {
|
||||||
|
u.Scheme = "https"
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(u)
|
||||||
|
config, err := createTLSConfiguration(endpointConfig.TLSCACertPath, endpointConfig.TLSCertPath, endpointConfig.TLSKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
proxy.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: config,
|
||||||
|
}
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSocketProxy(path string) http.Handler {
|
||||||
|
return &unixSocketHandler{path}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
|
||||||
|
type unixSocketHandler struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := net.Dial("unix", h.path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c := httputil.NewClientConn(conn, nil)
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
res, err := c.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
for k, vv := range res.Header {
|
||||||
|
for _, v := range vv {
|
||||||
|
w.Header().Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(w, res.Body); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler is a collection of all the service handlers.
|
||||||
|
type Handler struct {
|
||||||
|
AuthHandler *AuthHandler
|
||||||
|
UserHandler *UserHandler
|
||||||
|
SettingsHandler *SettingsHandler
|
||||||
|
TemplatesHandler *TemplatesHandler
|
||||||
|
DockerHandler *DockerHandler
|
||||||
|
WebSocketHandler *WebSocketHandler
|
||||||
|
FileHandler http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ErrInvalidJSON defines an error raised the app is unable to parse request data
|
||||||
|
ErrInvalidJSON = portainer.Error("Invalid JSON")
|
||||||
|
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
|
||||||
|
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||||
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasPrefix(r.URL.Path, "/api/auth") {
|
||||||
|
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
||||||
|
} else if strings.HasPrefix(r.URL.Path, "/api/users") {
|
||||||
|
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
|
||||||
|
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {
|
||||||
|
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
||||||
|
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
|
||||||
|
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||||
|
} else if strings.HasPrefix(r.URL.Path, "/api/websocket") {
|
||||||
|
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
||||||
|
} else if strings.HasPrefix(r.URL.Path, "/api/docker") {
|
||||||
|
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
|
||||||
|
} else if strings.HasPrefix(r.URL.Path, "/") {
|
||||||
|
h.FileHandler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error writes an API error message to the response and logger.
|
||||||
|
func Error(w http.ResponseWriter, err error, code int, logger *log.Logger) {
|
||||||
|
// Log error.
|
||||||
|
logger.Printf("http error: %s (code=%d)", err, code)
|
||||||
|
|
||||||
|
// Write generic error response.
|
||||||
|
w.WriteHeader(code)
|
||||||
|
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorResponse is a generic response for sending a error.
|
||||||
|
type errorResponse struct {
|
||||||
|
Err string `json:"err,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNotAllowed writes an API error message to the response and sets the Allow header.
|
||||||
|
func handleNotAllowed(w http.ResponseWriter, allowedMethods []string) {
|
||||||
|
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
json.NewEncoder(w).Encode(&errorResponse{Err: http.StatusText(http.StatusMethodNotAllowed)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeJSON encodes v to w in JSON format. Error() is called if encoding fails.
|
||||||
|
func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) {
|
||||||
|
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, logger)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,17 @@
|
||||||
package main
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"github.com/portainer/portainer"
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Service represents a service to manage HTTP middlewares
|
||||||
|
type middleWareService struct {
|
||||||
|
jwtService portainer.JWTService
|
||||||
|
}
|
||||||
|
|
||||||
func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
|
func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
|
||||||
for _, mw := range middleware {
|
for _, mw := range middleware {
|
||||||
h = mw(h)
|
h = mw(h)
|
||||||
|
@ -14,13 +19,27 @@ func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate provides Authentication middleware for handlers
|
func (service *middleWareService) addMiddleWares(h http.Handler) http.Handler {
|
||||||
func (api *api) authenticate(next http.Handler) http.Handler {
|
h = service.middleWareSecureHeaders(h)
|
||||||
|
h = service.middleWareAuthenticate(h)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// middleWareAuthenticate provides secure headers middleware for handlers
|
||||||
|
func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||||
|
w.Header().Add("X-Frame-Options", "DENY")
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// middleWareAuthenticate provides Authentication middleware for handlers
|
||||||
|
func (service *middleWareService) middleWareAuthenticate(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var token string
|
var token string
|
||||||
|
|
||||||
// Get token from the Authorization header
|
// Get token from the Authorization header
|
||||||
// format: Authorization: Bearer
|
|
||||||
tokens, ok := r.Header["Authorization"]
|
tokens, ok := r.Header["Authorization"]
|
||||||
if ok && len(tokens) >= 1 {
|
if ok && len(tokens) >= 1 {
|
||||||
token = tokens[0]
|
token = tokens[0]
|
||||||
|
@ -32,34 +51,13 @@ func (api *api) authenticate(next http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
err := service.jwtService.VerifyToken(token)
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
||||||
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
|
||||||
return nil, msg
|
|
||||||
}
|
|
||||||
return api.secret, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Invalid JWT token", http.StatusUnauthorized)
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsedToken == nil || !parsedToken.Valid {
|
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// context.Set(r, "user", parsedToken)
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecureHeaders adds secure headers to the API
|
|
||||||
func secureHeaders(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
|
||||||
w.Header().Add("X-Frame-Options", "DENY")
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server implements the portainer.Server interface
|
||||||
|
type Server struct {
|
||||||
|
BindAddress string
|
||||||
|
AssetsPath string
|
||||||
|
UserService portainer.UserService
|
||||||
|
CryptoService portainer.CryptoService
|
||||||
|
JWTService portainer.JWTService
|
||||||
|
Settings *portainer.Settings
|
||||||
|
TemplatesURL string
|
||||||
|
EndpointConfig *portainer.EndpointConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the HTTP server
|
||||||
|
func (server *Server) Start() error {
|
||||||
|
middleWareService := &middleWareService{
|
||||||
|
jwtService: server.JWTService,
|
||||||
|
}
|
||||||
|
var authHandler = NewAuthHandler()
|
||||||
|
authHandler.UserService = server.UserService
|
||||||
|
authHandler.CryptoService = server.CryptoService
|
||||||
|
authHandler.JWTService = server.JWTService
|
||||||
|
var userHandler = NewUserHandler(middleWareService)
|
||||||
|
userHandler.UserService = server.UserService
|
||||||
|
userHandler.CryptoService = server.CryptoService
|
||||||
|
var settingsHandler = NewSettingsHandler(middleWareService)
|
||||||
|
settingsHandler.settings = server.Settings
|
||||||
|
var templatesHandler = NewTemplatesHandler(middleWareService)
|
||||||
|
templatesHandler.templatesURL = server.TemplatesURL
|
||||||
|
var dockerHandler = NewDockerHandler(middleWareService)
|
||||||
|
dockerHandler.setupProxy(server.EndpointConfig)
|
||||||
|
var websocketHandler = NewWebSocketHandler()
|
||||||
|
websocketHandler.endpointConfiguration = server.EndpointConfig
|
||||||
|
var fileHandler = http.FileServer(http.Dir(server.AssetsPath))
|
||||||
|
|
||||||
|
handler := &Handler{
|
||||||
|
AuthHandler: authHandler,
|
||||||
|
UserHandler: userHandler,
|
||||||
|
SettingsHandler: settingsHandler,
|
||||||
|
TemplatesHandler: templatesHandler,
|
||||||
|
DockerHandler: dockerHandler,
|
||||||
|
WebSocketHandler: websocketHandler,
|
||||||
|
FileHandler: fileHandler,
|
||||||
|
}
|
||||||
|
return http.ListenAndServe(server.BindAddress, handler)
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SettingsHandler represents an HTTP API handler for managing settings.
|
||||||
|
type SettingsHandler struct {
|
||||||
|
*mux.Router
|
||||||
|
Logger *log.Logger
|
||||||
|
middleWareService *middleWareService
|
||||||
|
settings *portainer.Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSettingsHandler returns a new instance of SettingsHandler.
|
||||||
|
func NewSettingsHandler(middleWareService *middleWareService) *SettingsHandler {
|
||||||
|
h := &SettingsHandler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
|
middleWareService: middleWareService,
|
||||||
|
}
|
||||||
|
h.HandleFunc("/settings", h.handleGetSettings)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetSettings handles GET requests on /settings
|
||||||
|
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
handleNotAllowed(w, []string{"GET"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeJSON(w, handler.settings, handler.Logger)
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||||
|
type TemplatesHandler struct {
|
||||||
|
*mux.Router
|
||||||
|
Logger *log.Logger
|
||||||
|
middleWareService *middleWareService
|
||||||
|
templatesURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
||||||
|
func NewTemplatesHandler(middleWareService *middleWareService) *TemplatesHandler {
|
||||||
|
h := &TemplatesHandler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
|
middleWareService: middleWareService,
|
||||||
|
}
|
||||||
|
h.Handle("/templates", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handleGetTemplates(w, r)
|
||||||
|
})))
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetTemplates handles GET requests on /templates
|
||||||
|
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
handleNotAllowed(w, []string{"GET"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(handler.templatesURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
http.Error(w, fmt.Sprintf("Error making request to %s: %s", handler.templatesURL, err.Error()), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(body)
|
||||||
|
}
|
|
@ -1,27 +1,26 @@
|
||||||
package main
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// newTLSConfig initializes a tls.Config using a CA certificate, a certificate and a key
|
// createTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
|
||||||
func newTLSConfig(caCertPath, certPath, keyPath string) *tls.Config {
|
func createTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) {
|
||||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
caCert, err := ioutil.ReadFile(caCertPath)
|
caCert, err := ioutil.ReadFile(caCertPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
caCertPool := x509.NewCertPool()
|
caCertPool := x509.NewCertPool()
|
||||||
caCertPool.AppendCertsFromPEM(caCert)
|
caCertPool.AppendCertsFromPEM(caCert)
|
||||||
tlsConfig := &tls.Config{
|
config := &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
RootCAs: caCertPool,
|
RootCAs: caCertPool,
|
||||||
}
|
}
|
||||||
return tlsConfig
|
return config, nil
|
||||||
}
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserHandler represents an HTTP API handler for managing users.
|
||||||
|
type UserHandler struct {
|
||||||
|
*mux.Router
|
||||||
|
Logger *log.Logger
|
||||||
|
UserService portainer.UserService
|
||||||
|
CryptoService portainer.CryptoService
|
||||||
|
middleWareService *middleWareService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserHandler returns a new instance of UserHandler.
|
||||||
|
func NewUserHandler(middleWareService *middleWareService) *UserHandler {
|
||||||
|
h := &UserHandler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
|
middleWareService: middleWareService,
|
||||||
|
}
|
||||||
|
h.Handle("/users", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handlePostUsers(w, r)
|
||||||
|
})))
|
||||||
|
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handleGetUser(w, r)
|
||||||
|
}))).Methods("GET")
|
||||||
|
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handlePutUser(w, r)
|
||||||
|
}))).Methods("PUT")
|
||||||
|
h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handlePostUserPasswd(w, r)
|
||||||
|
})))
|
||||||
|
h.HandleFunc("/users/admin/check", h.handleGetAdminCheck)
|
||||||
|
h.HandleFunc("/users/admin/init", h.handlePostAdminInit)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePostUsers handles POST requests on /users
|
||||||
|
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
handleNotAllowed(w, []string{"POST"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req postUsersRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := govalidator.ValidateStruct(req)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &portainer.User{
|
||||||
|
Username: req.Username,
|
||||||
|
}
|
||||||
|
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.UserService.UpdateUser(user)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type postUsersRequest struct {
|
||||||
|
Username string `valid:"alphanum,required"`
|
||||||
|
Password string `valid:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePostUserPasswd handles POST requests on /users/:username/passwd
|
||||||
|
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
handleNotAllowed(w, []string{"POST"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
username := vars["username"]
|
||||||
|
|
||||||
|
var req postUserPasswdRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := govalidator.ValidateStruct(req)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var password = req.Password
|
||||||
|
|
||||||
|
u, err := handler.UserService.User(username)
|
||||||
|
if err == portainer.ErrUserNotFound {
|
||||||
|
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
valid := true
|
||||||
|
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
||||||
|
if err != nil {
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeJSON(w, &postUserPasswdResponse{Valid: valid}, handler.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
type postUserPasswdRequest struct {
|
||||||
|
Password string `valid:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type postUserPasswdResponse struct {
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetUser handles GET requests on /users/:username
|
||||||
|
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
username := vars["username"]
|
||||||
|
|
||||||
|
user, err := handler.UserService.User(username)
|
||||||
|
if err == portainer.ErrUserNotFound {
|
||||||
|
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Password = ""
|
||||||
|
encodeJSON(w, &user, handler.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePutUser handles PUT requests on /users/:username
|
||||||
|
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req putUserRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := govalidator.ValidateStruct(req)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &portainer.User{
|
||||||
|
Username: req.Username,
|
||||||
|
}
|
||||||
|
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.UserService.UpdateUser(user)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type putUserRequest struct {
|
||||||
|
Username string `valid:"alphanum,required"`
|
||||||
|
Password string `valid:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePostAdminInit handles GET requests on /users/admin/check
|
||||||
|
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
handleNotAllowed(w, []string{"GET"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := handler.UserService.User("admin")
|
||||||
|
if err == portainer.ErrUserNotFound {
|
||||||
|
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Password = ""
|
||||||
|
encodeJSON(w, &user, handler.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePostAdminInit handles POST requests on /users/admin/init
|
||||||
|
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
handleNotAllowed(w, []string{"POST"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req postAdminInitRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := govalidator.ValidateStruct(req)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &portainer.User{
|
||||||
|
Username: "admin",
|
||||||
|
}
|
||||||
|
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.UserService.UpdateUser(user)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type postAdminInitRequest struct {
|
||||||
|
Password string `valid:"required"`
|
||||||
|
}
|
|
@ -1,17 +1,78 @@
|
||||||
package main
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
|
||||||
|
type WebSocketHandler struct {
|
||||||
|
*mux.Router
|
||||||
|
Logger *log.Logger
|
||||||
|
middleWareService *middleWareService
|
||||||
|
endpointConfiguration *portainer.EndpointConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWebSocketHandler returns a new instance of WebSocketHandler.
|
||||||
|
func NewWebSocketHandler() *WebSocketHandler {
|
||||||
|
h := &WebSocketHandler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
|
}
|
||||||
|
h.Handle("/websocket/exec", websocket.Handler(h.webSocketDockerExec))
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
||||||
|
qry := ws.Request().URL.Query()
|
||||||
|
execID := qry.Get("id")
|
||||||
|
|
||||||
|
// Should not be managed here
|
||||||
|
endpoint, err := url.Parse(handler.endpointConfiguration.Endpoint)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to parse endpoint URL: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var host string
|
||||||
|
if endpoint.Scheme == "tcp" {
|
||||||
|
host = endpoint.Host
|
||||||
|
} else if endpoint.Scheme == "unix" {
|
||||||
|
host = endpoint.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not be managed here
|
||||||
|
var tlsConfig *tls.Config
|
||||||
|
if handler.endpointConfiguration.TLS {
|
||||||
|
tlsConfig, err = createTLSConfiguration(handler.endpointConfiguration.TLSCACertPath,
|
||||||
|
handler.endpointConfiguration.TLSCertPath,
|
||||||
|
handler.endpointConfiguration.TLSKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hijack(host, endpoint.Scheme, "POST", "/exec/"+execID+"/start", tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
|
||||||
|
log.Fatalf("error during hijack: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type execConfig struct {
|
type execConfig struct {
|
||||||
Tty bool
|
Tty bool
|
||||||
Detach bool
|
Detach bool
|
29
api/jwt.go
29
api/jwt.go
|
@ -1,29 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type claims struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
jwt.StandardClaims
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *api) generateJWTToken(username string) (string, error) {
|
|
||||||
expireToken := time.Now().Add(time.Hour * 8).Unix()
|
|
||||||
claims := claims{
|
|
||||||
username,
|
|
||||||
jwt.StandardClaims{
|
|
||||||
ExpiresAt: expireToken,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
||||||
|
|
||||||
signedToken, err := token.SignedString(api.secret)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return signedToken, nil
|
|
||||||
}
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service represents a service for managing JWT tokens.
|
||||||
|
type Service struct {
|
||||||
|
secret []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type claims struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
jwt.StandardClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService initializes a new service. It will generate a random key that will be used to sign JWT tokens.
|
||||||
|
func NewService() (*Service, error) {
|
||||||
|
secret := securecookie.GenerateRandomKey(32)
|
||||||
|
if secret == nil {
|
||||||
|
return nil, portainer.ErrSecretGeneration
|
||||||
|
}
|
||||||
|
service := &Service{
|
||||||
|
secret,
|
||||||
|
}
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken generates a new JWT token.
|
||||||
|
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
|
||||||
|
expireToken := time.Now().Add(time.Hour * 8).Unix()
|
||||||
|
cl := claims{
|
||||||
|
data.Username,
|
||||||
|
jwt.StandardClaims{
|
||||||
|
ExpiresAt: expireToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
|
||||||
|
|
||||||
|
signedToken, err := token.SignedString(service.secret)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
|
||||||
|
func (service *Service) VerifyToken(token string) error {
|
||||||
|
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||||
|
return nil, msg
|
||||||
|
}
|
||||||
|
return service.secret, nil
|
||||||
|
})
|
||||||
|
if err != nil || parsedToken == nil || !parsedToken.Valid {
|
||||||
|
return portainer.ErrInvalidJWTToken
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
52
api/main.go
52
api/main.go
|
@ -1,52 +0,0 @@
|
||||||
package main // import "github.com/portainer/portainer"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Version number of portainer API
|
|
||||||
Version = "1.10.2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// main is the entry point of the program
|
|
||||||
func main() {
|
|
||||||
kingpin.Version(Version)
|
|
||||||
var (
|
|
||||||
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
|
|
||||||
addr = kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String()
|
|
||||||
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
|
|
||||||
data = kingpin.Flag("data", "Path to the folder where the data is stored").Default("/data").Short('d').String()
|
|
||||||
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
|
|
||||||
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
|
|
||||||
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
|
|
||||||
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
|
|
||||||
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
|
|
||||||
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
|
|
||||||
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
|
|
||||||
templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String()
|
|
||||||
)
|
|
||||||
kingpin.Parse()
|
|
||||||
|
|
||||||
apiConfig := apiConfig{
|
|
||||||
Endpoint: *endpoint,
|
|
||||||
BindAddress: *addr,
|
|
||||||
AssetPath: *assets,
|
|
||||||
DataPath: *data,
|
|
||||||
SwarmSupport: *swarm,
|
|
||||||
TLSEnabled: *tlsverify,
|
|
||||||
TLSCACertPath: *tlscacert,
|
|
||||||
TLSCertPath: *tlscert,
|
|
||||||
TLSKeyPath: *tlskey,
|
|
||||||
TemplatesURL: *templates,
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := &Settings{
|
|
||||||
Swarm: *swarm,
|
|
||||||
HiddenLabels: *labels,
|
|
||||||
Logo: *logo,
|
|
||||||
}
|
|
||||||
|
|
||||||
api := newAPI(apiConfig)
|
|
||||||
api.run(settings)
|
|
||||||
}
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
package portainer
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Pair defines a key/value string pair
|
||||||
|
Pair struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLIFlags represents the available flags on the CLI.
|
||||||
|
CLIFlags struct {
|
||||||
|
Addr *string
|
||||||
|
Assets *string
|
||||||
|
Data *string
|
||||||
|
Endpoint *string
|
||||||
|
Labels *[]Pair
|
||||||
|
Logo *string
|
||||||
|
Swarm *bool
|
||||||
|
Templates *string
|
||||||
|
TLSVerify *bool
|
||||||
|
TLSCacert *string
|
||||||
|
TLSCert *string
|
||||||
|
TLSKey *string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings represents Portainer settings.
|
||||||
|
Settings struct {
|
||||||
|
Swarm bool `json:"swarm"`
|
||||||
|
HiddenLabels []Pair `json:"hiddenLabels"`
|
||||||
|
Logo string `json:"logo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User represent a user account.
|
||||||
|
User struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenData represents the data embedded in a JWT token.
|
||||||
|
TokenData struct {
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndpointConfiguration represents the data required to connect to a Docker API endpoint.
|
||||||
|
EndpointConfiguration struct {
|
||||||
|
Endpoint string
|
||||||
|
TLS bool
|
||||||
|
TLSCACertPath string
|
||||||
|
TLSCertPath string
|
||||||
|
TLSKeyPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLIService represents a service for managing CLI.
|
||||||
|
CLIService interface {
|
||||||
|
ParseFlags(version string) (*CLIFlags, error)
|
||||||
|
ValidateFlags(flags *CLIFlags) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataStore defines the interface to manage the data.
|
||||||
|
DataStore interface {
|
||||||
|
Open() error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server defines the interface to serve the data.
|
||||||
|
Server interface {
|
||||||
|
Start() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserService represents a service for managing users.
|
||||||
|
UserService interface {
|
||||||
|
User(username string) (*User, error)
|
||||||
|
UpdateUser(user *User) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CryptoService represents a service for encrypting/hashing data.
|
||||||
|
CryptoService interface {
|
||||||
|
Hash(data string) (string, error)
|
||||||
|
CompareHashAndData(hash string, data string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTService represents a service for managing JWT tokens.
|
||||||
|
JWTService interface {
|
||||||
|
GenerateToken(data *TokenData) (string, error)
|
||||||
|
VerifyToken(token string) error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// APIVersion is the version number of portainer API.
|
||||||
|
APIVersion = "1.10.2"
|
||||||
|
)
|
|
@ -1,18 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Settings defines the settings available under the /settings endpoint
|
|
||||||
type Settings struct {
|
|
||||||
Swarm bool `json:"swarm"`
|
|
||||||
HiddenLabels pairList `json:"hiddenLabels"`
|
|
||||||
Logo string `json:"logo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// settingsHandler defines a handler function used to encode the configuration in JSON
|
|
||||||
func settingsHandler(w http.ResponseWriter, r *http.Request, s *Settings) {
|
|
||||||
json.NewEncoder(w).Encode(*s)
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// templatesHandler defines a handler function used to retrieve the templates from a URL and put them in the response
|
|
||||||
func templatesHandler(w http.ResponseWriter, r *http.Request, templatesURL string) {
|
|
||||||
resp, err := http.Get(templatesURL)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, fmt.Sprintf("Error making request to %s: %s", templatesURL, err.Error()), http.StatusInternalServerError)
|
|
||||||
log.Print(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
|
|
||||||
log.Print(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(body)
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
)
|
|
||||||
|
|
||||||
// unixHandler defines a handler holding the path to a socket under UNIX
|
|
||||||
type unixHandler struct {
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP implementation for unixHandler
|
|
||||||
func (h *unixHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
conn, err := net.Dial("unix", h.path)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c := httputil.NewClientConn(conn, nil)
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
res, err := c.Do(r)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
copyHeader(w.Header(), res.Header)
|
|
||||||
if _, err := io.Copy(w, res.Body); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyHeader(dst, src http.Header) {
|
|
||||||
for k, vv := range src {
|
|
||||||
for _, v := range vv {
|
|
||||||
dst.Add(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
219
api/users.go
219
api/users.go
|
@ -1,219 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
passwordCheckRequest struct {
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
passwordCheckResponse struct {
|
|
||||||
Valid bool `json:"valid"`
|
|
||||||
}
|
|
||||||
initAdminRequest struct {
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// handle /users
|
|
||||||
// Allowed methods: POST
|
|
||||||
func (api *api) usersHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" {
|
|
||||||
w.Header().Set("Allow", "POST")
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var user userItem
|
|
||||||
err = json.Unmarshal(body, &user)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user.Password, err = hashPassword(user.Password)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to hash user password", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = api.dataStore.updateUser(user)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Unable to persist user: %s", err.Error())
|
|
||||||
http.Error(w, "Unable to persist user", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle /users/admin/check
|
|
||||||
// Allowed methods: POST
|
|
||||||
func (api *api) checkAdminHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "GET" {
|
|
||||||
w.Header().Set("Allow", "GET")
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := api.dataStore.getUserByUsername("admin")
|
|
||||||
if err == errUserNotFound {
|
|
||||||
log.Printf("User not found: %s", "admin")
|
|
||||||
http.Error(w, "User not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Unable to retrieve user: %s", err.Error())
|
|
||||||
http.Error(w, "Unable to retrieve user", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user.Password = ""
|
|
||||||
json.NewEncoder(w).Encode(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle /users/admin/init
|
|
||||||
// Allowed methods: POST
|
|
||||||
func (api *api) initAdminHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" {
|
|
||||||
w.Header().Set("Allow", "POST")
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestData initAdminRequest
|
|
||||||
err = json.Unmarshal(body, &requestData)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := userItem{
|
|
||||||
Username: "admin",
|
|
||||||
}
|
|
||||||
user.Password, err = hashPassword(requestData.Password)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to hash user password", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = api.dataStore.updateUser(user)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Unable to persist user: %s", err.Error())
|
|
||||||
http.Error(w, "Unable to persist user", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle /users/{username}
|
|
||||||
// Allowed methods: PUT, GET
|
|
||||||
func (api *api) userHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == "PUT" {
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var user userItem
|
|
||||||
err = json.Unmarshal(body, &user)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user.Password, err = hashPassword(user.Password)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to hash user password", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = api.dataStore.updateUser(user)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Unable to persist user: %s", err.Error())
|
|
||||||
http.Error(w, "Unable to persist user", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if r.Method == "GET" {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
username := vars["username"]
|
|
||||||
|
|
||||||
user, err := api.dataStore.getUserByUsername(username)
|
|
||||||
if err == errUserNotFound {
|
|
||||||
log.Printf("User not found: %s", username)
|
|
||||||
http.Error(w, "User not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Unable to retrieve user: %s", err.Error())
|
|
||||||
http.Error(w, "Unable to retrieve user", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user.Password = ""
|
|
||||||
json.NewEncoder(w).Encode(user)
|
|
||||||
} else {
|
|
||||||
w.Header().Set("Allow", "PUT, GET")
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle /users/{username}/passwd
|
|
||||||
// Allowed methods: POST
|
|
||||||
func (api *api) userPasswordHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" {
|
|
||||||
w.Header().Set("Allow", "POST")
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
username := vars["username"]
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to parse request body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var data passwordCheckRequest
|
|
||||||
err = json.Unmarshal(body, &data)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Unable to parse user data", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := api.dataStore.getUserByUsername(username)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Unable to retrieve user: %s", err.Error())
|
|
||||||
http.Error(w, "Unable to retrieve user", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
valid := true
|
|
||||||
err = checkPasswordValidity(data.Password, user.Password)
|
|
||||||
if err != nil {
|
|
||||||
valid = false
|
|
||||||
}
|
|
||||||
|
|
||||||
response := passwordCheckResponse{
|
|
||||||
Valid: valid,
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(response)
|
|
||||||
}
|
|
|
@ -498,10 +498,11 @@ angular.module('portainer', [
|
||||||
}])
|
}])
|
||||||
// This is your docker url that the api will use to make requests
|
// This is your docker url that the api will use to make requests
|
||||||
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
||||||
.constant('DOCKER_ENDPOINT', 'dockerapi')
|
.constant('DOCKER_ENDPOINT', '/api/docker')
|
||||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
||||||
.constant('CONFIG_ENDPOINT', 'settings')
|
.constant('CONFIG_ENDPOINT', '/api/settings')
|
||||||
.constant('AUTH_ENDPOINT', 'auth')
|
.constant('AUTH_ENDPOINT', '/api/auth')
|
||||||
.constant('TEMPLATES_ENDPOINT', 'templates')
|
.constant('USERS_ENDPOINT', '/api/users')
|
||||||
|
.constant('TEMPLATES_ENDPOINT', '/api/templates')
|
||||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||||
.constant('UI_VERSION', 'v1.10.2');
|
.constant('UI_VERSION', 'v1.10.2');
|
||||||
|
|
|
@ -55,7 +55,7 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Mess
|
||||||
} else {
|
} else {
|
||||||
var execId = d.Id;
|
var execId = d.Id;
|
||||||
resizeTTY(execId, termHeight, termWidth);
|
resizeTTY(execId, termHeight, termWidth);
|
||||||
var url = window.location.href.split('#')[0] + 'ws/exec?id=' + execId;
|
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId;
|
||||||
if (url.indexOf('https') > -1) {
|
if (url.indexOf('https') > -1) {
|
||||||
url = url.replace('https://', 'wss://');
|
url = url.replace('https://', 'wss://');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -229,9 +229,9 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}])
|
}])
|
||||||
.factory('Users', ['$resource', function UsersFactory($resource) {
|
.factory('Users', ['$resource', 'USERS_ENDPOINT', function UsersFactory($resource, USERS_ENDPOINT) {
|
||||||
'use strict';
|
'use strict';
|
||||||
return $resource('/users/:username/:action', {}, {
|
return $resource(USERS_ENDPOINT + '/:username/:action', {}, {
|
||||||
create: { method: 'POST' },
|
create: { method: 'POST' },
|
||||||
get: {method: 'GET', params: { username: '@username' } },
|
get: {method: 'GET', params: { username: '@username' } },
|
||||||
update: { method: 'PUT', params: { username: '@username' } },
|
update: { method: 'PUT', params: { username: '@username' } },
|
||||||
|
|
24
gruntFile.js
24
gruntFile.js
|
@ -297,34 +297,34 @@ module.exports = function (grunt) {
|
||||||
},
|
},
|
||||||
buildBinary: {
|
buildBinary: {
|
||||||
command: [
|
command: [
|
||||||
'docker run --rm -v $(pwd)/api:/src portainer/golang-builder',
|
'docker run --rm -v $(pwd)/api:/src portainer/golang-builder /src/cmd/portainer',
|
||||||
'shasum api/portainer > portainer-checksum.txt',
|
'shasum api/cmd/portainer/portainer > portainer-checksum.txt',
|
||||||
'mkdir -p dist',
|
'mkdir -p dist',
|
||||||
'mv api/portainer dist/'
|
'mv api/cmd/portainer/portainer dist/'
|
||||||
].join(' && ')
|
].join(' && ')
|
||||||
},
|
},
|
||||||
buildUnixArmBinary: {
|
buildUnixArmBinary: {
|
||||||
command: [
|
command: [
|
||||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" portainer/golang-builder:cross-platform',
|
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" portainer/golang-builder:cross-platform /src/cmd/portainer',
|
||||||
'shasum api/portainer-linux-arm > portainer-checksum.txt',
|
'shasum api/cmd/portainer/portainer-linux-arm > portainer-checksum.txt',
|
||||||
'mkdir -p dist',
|
'mkdir -p dist',
|
||||||
'mv api/portainer-linux-arm dist/portainer'
|
'mv api/cmd/portainer/portainer-linux-arm dist/portainer'
|
||||||
].join(' && ')
|
].join(' && ')
|
||||||
},
|
},
|
||||||
buildDarwinBinary: {
|
buildDarwinBinary: {
|
||||||
command: [
|
command: [
|
||||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform',
|
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform /src/cmd/portainer',
|
||||||
'shasum api/portainer-darwin-amd64 > portainer-checksum.txt',
|
'shasum api/cmd/portainer/portainer-darwin-amd64 > portainer-checksum.txt',
|
||||||
'mkdir -p dist',
|
'mkdir -p dist',
|
||||||
'mv api/portainer-darwin-amd64 dist/portainer'
|
'mv api/cmd/portainer/portainer-darwin-amd64 dist/portainer'
|
||||||
].join(' && ')
|
].join(' && ')
|
||||||
},
|
},
|
||||||
buildWindowsBinary: {
|
buildWindowsBinary: {
|
||||||
command: [
|
command: [
|
||||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform',
|
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform /src/cmd/portainer',
|
||||||
'shasum api/portainer-windows-amd64 > portainer-checksum.txt',
|
'shasum api/cmd/portainer/portainer-windows-amd64 > portainer-checksum.txt',
|
||||||
'mkdir -p dist',
|
'mkdir -p dist',
|
||||||
'mv api/portainer-windows-amd64 dist/portainer.exe'
|
'mv api/cmd/portainer/portainer-windows-amd64 dist/portainer.exe'
|
||||||
].join(' && ')
|
].join(' && ')
|
||||||
},
|
},
|
||||||
run: {
|
run: {
|
||||||
|
|
Loading…
Reference in New Issue