refactor(api): API overhaul (#392)
parent
d9f6124609
commit
0a38bba874
|
@ -2,3 +2,4 @@ node_modules
|
|||
bower_components
|
||||
dist
|
||||
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 (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"fmt"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// pair defines a key/value pair
|
||||
type pair struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
type pairList []portainer.Pair
|
||||
|
||||
// pairList defines an array of Label
|
||||
type pairList []pair
|
||||
|
||||
// Set implementation for Labels
|
||||
// Set implementation for a list of portainer.Pair
|
||||
func (l *pairList) Set(value string) error {
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("expected NAME=VALUE got '%s'", value)
|
||||
}
|
||||
p := new(pair)
|
||||
p := new(portainer.Pair)
|
||||
p.Name = parts[0]
|
||||
p.Value = parts[1]
|
||||
*l = append(*l, *p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implementation for Labels
|
||||
// String implementation for a list of pair
|
||||
func (l *pairList) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsCumulative implementation for Labels
|
||||
// IsCumulative implementation for a list of pair
|
||||
func (l *pairList) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// LabelParser defines a custom parser for Labels flags
|
||||
func pairs(s kingpin.Settings) (target *[]pair) {
|
||||
target = new([]pair)
|
||||
func pairs(s kingpin.Settings) (target *[]portainer.Pair) {
|
||||
target = new([]portainer.Pair)
|
||||
s.SetValue((*pairList)(target))
|
||||
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 (
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"net/http"
|
||||
"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 {
|
||||
for _, mw := range middleware {
|
||||
h = mw(h)
|
||||
|
@ -14,13 +19,27 @@ func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler
|
|||
return h
|
||||
}
|
||||
|
||||
// authenticate provides Authentication middleware for handlers
|
||||
func (api *api) authenticate(next http.Handler) http.Handler {
|
||||
func (service *middleWareService) addMiddleWares(h 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) {
|
||||
var token string
|
||||
|
||||
// Get token from the Authorization header
|
||||
// format: Authorization: Bearer
|
||||
tokens, ok := r.Header["Authorization"]
|
||||
if ok && len(tokens) >= 1 {
|
||||
token = tokens[0]
|
||||
|
@ -32,34 +51,13 @@ func (api *api) authenticate(next http.Handler) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
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 api.secret, nil
|
||||
})
|
||||
err := service.jwtService.VerifyToken(token)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid JWT token", http.StatusUnauthorized)
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
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)
|
||||
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 (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
)
|
||||
|
||||
// newTLSConfig initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
func newTLSConfig(caCertPath, certPath, keyPath string) *tls.Config {
|
||||
// createTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
func createTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
caCert, err := ioutil.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
tlsConfig := &tls.Config{
|
||||
config := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
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 (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/net/websocket"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"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 {
|
||||
Tty 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
|
||||
// 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('CONFIG_ENDPOINT', 'settings')
|
||||
.constant('AUTH_ENDPOINT', 'auth')
|
||||
.constant('TEMPLATES_ENDPOINT', 'templates')
|
||||
.constant('CONFIG_ENDPOINT', '/api/settings')
|
||||
.constant('AUTH_ENDPOINT', '/api/auth')
|
||||
.constant('USERS_ENDPOINT', '/api/users')
|
||||
.constant('TEMPLATES_ENDPOINT', '/api/templates')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('UI_VERSION', 'v1.10.2');
|
||||
|
|
|
@ -55,7 +55,7 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Mess
|
|||
} else {
|
||||
var execId = d.Id;
|
||||
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) {
|
||||
url = url.replace('https://', 'wss://');
|
||||
} 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';
|
||||
return $resource('/users/:username/:action', {}, {
|
||||
return $resource(USERS_ENDPOINT + '/:username/:action', {}, {
|
||||
create: { method: 'POST' },
|
||||
get: {method: 'GET', params: { username: '@username' } },
|
||||
update: { method: 'PUT', params: { username: '@username' } },
|
||||
|
|
24
gruntFile.js
24
gruntFile.js
|
@ -297,34 +297,34 @@ module.exports = function (grunt) {
|
|||
},
|
||||
buildBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src portainer/golang-builder',
|
||||
'shasum api/portainer > portainer-checksum.txt',
|
||||
'docker run --rm -v $(pwd)/api:/src portainer/golang-builder /src/cmd/portainer',
|
||||
'shasum api/cmd/portainer/portainer > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/portainer dist/'
|
||||
'mv api/cmd/portainer/portainer dist/'
|
||||
].join(' && ')
|
||||
},
|
||||
buildUnixArmBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" portainer/golang-builder:cross-platform',
|
||||
'shasum api/portainer-linux-arm > portainer-checksum.txt',
|
||||
'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/cmd/portainer/portainer-linux-arm > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/portainer-linux-arm dist/portainer'
|
||||
'mv api/cmd/portainer/portainer-linux-arm dist/portainer'
|
||||
].join(' && ')
|
||||
},
|
||||
buildDarwinBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform',
|
||||
'shasum api/portainer-darwin-amd64 > portainer-checksum.txt',
|
||||
'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/cmd/portainer/portainer-darwin-amd64 > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/portainer-darwin-amd64 dist/portainer'
|
||||
'mv api/cmd/portainer/portainer-darwin-amd64 dist/portainer'
|
||||
].join(' && ')
|
||||
},
|
||||
buildWindowsBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform',
|
||||
'shasum api/portainer-windows-amd64 > portainer-checksum.txt',
|
||||
'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/cmd/portainer/portainer-windows-amd64 > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv api/portainer-windows-amd64 dist/portainer.exe'
|
||||
'mv api/cmd/portainer/portainer-windows-amd64 dist/portainer.exe'
|
||||
].join(' && ')
|
||||
},
|
||||
run: {
|
||||
|
|
Loading…
Reference in New Issue