mirror of
https://github.com/bytedream/docker4ssh.git
synced 2025-06-27 09:50:31 +02:00
Initial commit
This commit is contained in:
123
server/api/api.go
Normal file
123
server/api/api.go
Normal file
@ -0,0 +1,123 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"docker4ssh/config"
|
||||
"docker4ssh/ssh"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EndpointHandler struct {
|
||||
http.Handler
|
||||
|
||||
auth bool
|
||||
|
||||
get func(http.ResponseWriter, *http.Request, *ssh.User) (interface{}, int)
|
||||
post func(http.ResponseWriter, *http.Request, *ssh.User) (interface{}, int)
|
||||
}
|
||||
|
||||
func (h *EndpointHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ip := strings.Split(r.RemoteAddr, ":")[0]
|
||||
|
||||
zap.S().Infof("User connected to api with remote address %s", ip)
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
user := ssh.GetUser(ip)
|
||||
// checks if auth should be checked and if so and no user could be found, response an error
|
||||
if h.auth && user == nil {
|
||||
zap.S().Errorf("Could not find api user with ip %s", ip)
|
||||
json.NewEncoder(w).Encode(APIError{Message: "unauthorized"})
|
||||
return
|
||||
} else if user != nil {
|
||||
zap.S().Debugf("API ip %s is %s", ip, user.ID)
|
||||
}
|
||||
|
||||
raw := bytes.Buffer{}
|
||||
if r.ContentLength > 0 {
|
||||
io.Copy(&raw, r.Body)
|
||||
defer r.Body.Close()
|
||||
if !json.Valid(raw.Bytes()) {
|
||||
zap.S().Errorf("API user %s sent invalid body", ip)
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
json.NewEncoder(w).Encode(APIError{Message: "invalid body"})
|
||||
return
|
||||
}
|
||||
r.Body = ioutil.NopCloser(&raw)
|
||||
}
|
||||
|
||||
zap.S().Debugf("API user %s request - \"%s %s %s\" \"%s\" \"%s\"", ip, r.Method, r.URL.Path, r.Proto, r.UserAgent(), raw.String())
|
||||
|
||||
var response interface{}
|
||||
var code int
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if h.get != nil {
|
||||
response, code = h.get(w, r, user)
|
||||
}
|
||||
case http.MethodPost:
|
||||
if h.post != nil {
|
||||
response, code = h.post(w, r, user)
|
||||
}
|
||||
}
|
||||
|
||||
if response == nil && code == 0 {
|
||||
zap.S().Infof("API user %s sent invalid method: %s", ip, r.Method)
|
||||
response = APIError{Message: fmt.Sprintf("invalid method '%s'", r.Method)}
|
||||
code = http.StatusConflict
|
||||
} else {
|
||||
zap.S().Infof("API user %s issued %s successfully", ip, r.URL.Path)
|
||||
}
|
||||
|
||||
w.WriteHeader(code)
|
||||
if response != nil {
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
}
|
||||
|
||||
func ServeAPI(config *config.Config) (errChan chan error, closer func() error) {
|
||||
errChan = make(chan error, 1)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.Handle("/ping", &EndpointHandler{
|
||||
get: PingGet,
|
||||
})
|
||||
mux.Handle("/error", &EndpointHandler{
|
||||
get: ErrorGet,
|
||||
})
|
||||
mux.Handle("/info", &EndpointHandler{
|
||||
get: InfoGet,
|
||||
auth: true,
|
||||
})
|
||||
mux.Handle("/config", &EndpointHandler{
|
||||
get: ConfigGet,
|
||||
post: ConfigPost,
|
||||
auth: true,
|
||||
})
|
||||
mux.Handle("/auth", &EndpointHandler{
|
||||
get: AuthGet,
|
||||
post: AuthPost,
|
||||
auth: true,
|
||||
})
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", config.Api.Port))
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
errChan <- http.Serve(listener, mux)
|
||||
}()
|
||||
|
||||
return errChan, listener.Close
|
||||
}
|
80
server/api/auth.go
Normal file
80
server/api/auth.go
Normal file
@ -0,0 +1,80 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"docker4ssh/database"
|
||||
"docker4ssh/ssh"
|
||||
"encoding/json"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type authGetResponse struct {
|
||||
User string `json:"user"`
|
||||
HasPassword bool `json:"has_password"`
|
||||
}
|
||||
|
||||
func AuthGet(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) {
|
||||
auth, ok := database.GetDatabase().GetAuthByContainer(user.Container.FullContainerID)
|
||||
|
||||
if ok {
|
||||
return authGetResponse{
|
||||
User: *auth.User,
|
||||
HasPassword: auth.Password != nil,
|
||||
}, http.StatusOK
|
||||
} else {
|
||||
return APIError{Message: "no auth is set"}, http.StatusNotFound
|
||||
}
|
||||
}
|
||||
|
||||
type authPostRequest struct {
|
||||
User *string `json:"user"`
|
||||
Password *string `json:"password"`
|
||||
}
|
||||
|
||||
func AuthPost(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) {
|
||||
var request authPostRequest
|
||||
json.NewDecoder(r.Body).Decode(&request)
|
||||
defer r.Body.Close()
|
||||
|
||||
db := database.GetDatabase()
|
||||
|
||||
auth, _ := db.GetAuthByContainer(user.Container.FullContainerID)
|
||||
|
||||
if request.User != nil {
|
||||
if *request.User == "" {
|
||||
return APIError{Message: "new username cannot be empty"}, http.StatusNotAcceptable
|
||||
}
|
||||
if err := db.SetAuth(user.Container.FullContainerID, database.Auth{
|
||||
User: request.User,
|
||||
}); err != nil {
|
||||
zap.S().Errorf("Error while updating user for user %s: %v", user.ID, err)
|
||||
return APIError{Message: "failed to process user"}, http.StatusInternalServerError
|
||||
}
|
||||
zap.S().Infof("Updated password for %s", user.Container.ContainerID)
|
||||
}
|
||||
if request.Password != nil && *request.Password == "" {
|
||||
if err := db.DeleteAuth(user.Container.FullContainerID); err != nil {
|
||||
zap.S().Errorf("Error while deleting auth for user %s: %v", user.ID, err)
|
||||
return APIError{Message: "failed to delete auth"}, http.StatusInternalServerError
|
||||
}
|
||||
zap.S().Infof("Deleted authenticiation for %s", user.Container.ContainerID)
|
||||
} else if request.Password != nil {
|
||||
pwd, err := bcrypt.GenerateFromPassword([]byte(*request.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
zap.S().Errorf("Error while updating password for user %s: %v", user.ID, err)
|
||||
return APIError{Message: "failed to process password"}, http.StatusInternalServerError
|
||||
}
|
||||
var username string
|
||||
if auth.User == nil {
|
||||
username = user.Container.FullContainerID
|
||||
} else {
|
||||
username = *auth.User
|
||||
}
|
||||
if err = db.SetAuth(user.Container.FullContainerID, database.NewUnsafeAuth(username, pwd)); err != nil {
|
||||
return APIError{Message: "failed to update authentication"}, http.StatusInternalServerError
|
||||
}
|
||||
zap.S().Infof("Updated password for %s", user.Container.ContainerID)
|
||||
}
|
||||
return nil, http.StatusOK
|
||||
}
|
124
server/api/config.go
Normal file
124
server/api/config.go
Normal file
@ -0,0 +1,124 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"docker4ssh/docker"
|
||||
"docker4ssh/ssh"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type configGetResponse struct {
|
||||
NetworkMode docker.NetworkMode `json:"network_mode"`
|
||||
Configurable bool `json:"configurable"`
|
||||
RunLevel docker.RunLevel `json:"run_level"`
|
||||
StartupInformation bool `json:"startup_information"`
|
||||
ExitAfter string `json:"exit_after"`
|
||||
KeepOnExit bool `json:"keep_on_exit"`
|
||||
}
|
||||
|
||||
func ConfigGet(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) {
|
||||
config := user.Container.Config()
|
||||
|
||||
return configGetResponse{
|
||||
config.NetworkMode,
|
||||
config.Configurable,
|
||||
config.RunLevel,
|
||||
config.StartupInformation,
|
||||
config.ExitAfter,
|
||||
config.KeepOnExit,
|
||||
}, http.StatusOK
|
||||
}
|
||||
|
||||
type configPostRequest configGetResponse
|
||||
|
||||
var configPostRequestLookup, _ = structJsonLookup(configPostRequest{})
|
||||
|
||||
type configPostResponse struct {
|
||||
Message string `json:"message"`
|
||||
Rejected []configPostResponseRejected `json:"rejected"`
|
||||
}
|
||||
|
||||
type configPostResponseRejected struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func ConfigPost(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) {
|
||||
var requestBody map[string]interface{}
|
||||
json.NewDecoder(r.Body).Decode(&requestBody)
|
||||
defer r.Body.Close()
|
||||
|
||||
var change bool
|
||||
var response configPostResponse
|
||||
|
||||
updatedConfig := user.Container.Config()
|
||||
|
||||
for k, v := range requestBody {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
kind, ok := configPostRequestLookup[k]
|
||||
if !ok {
|
||||
response.Rejected = append(response.Rejected, configPostResponseRejected{
|
||||
Name: k,
|
||||
Description: fmt.Sprintf("name / field %s does not exist", k),
|
||||
})
|
||||
} else {
|
||||
valueKind := reflect.TypeOf(v).Kind()
|
||||
if valueKind != kind && valueKind == reflect.Float64 && kind == reflect.Int {
|
||||
valueKind = reflect.Int
|
||||
}
|
||||
|
||||
if valueKind != kind {
|
||||
response.Rejected = append(response.Rejected, configPostResponseRejected{
|
||||
Name: k,
|
||||
Description: fmt.Sprintf("value should be type %s, got type %s", kind, valueKind),
|
||||
})
|
||||
}
|
||||
|
||||
change = true
|
||||
switch k {
|
||||
case "network_mode":
|
||||
updatedConfig.NetworkMode = docker.NetworkMode(v.(float64))
|
||||
case "configurable":
|
||||
updatedConfig.Configurable = v.(bool)
|
||||
case "run_level":
|
||||
updatedConfig.RunLevel = docker.RunLevel(v.(float64))
|
||||
case "startup_information":
|
||||
updatedConfig.StartupInformation = v.(bool)
|
||||
case "exit_after":
|
||||
updatedConfig.ExitAfter = v.(string)
|
||||
case "keep_on_exit":
|
||||
updatedConfig.KeepOnExit = v.(bool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(response.Rejected) > 0 {
|
||||
var arr []string
|
||||
for _, rejected := range response.Rejected {
|
||||
arr = append(arr, rejected.Name)
|
||||
}
|
||||
|
||||
if len(response.Rejected) == 1 {
|
||||
response.Message = fmt.Sprintf("1 invalid configuration was found: %s", strings.Join(arr, ", "))
|
||||
return response, http.StatusNotAcceptable
|
||||
} else if len(response.Rejected) > 1 {
|
||||
response.Message = fmt.Sprintf("%d invalid configurations were found: %s", len(response.Rejected), strings.Join(arr, ", "))
|
||||
return response, http.StatusNotAcceptable
|
||||
}
|
||||
} else if change {
|
||||
if err := user.Container.UpdateConfig(context.Background(), updatedConfig); err != nil {
|
||||
zap.S().Errorf("Error while updating config for API user %s: %v", user.ID, err)
|
||||
response.Message = "Internal error while updating the config"
|
||||
return response, http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
return nil, http.StatusOK
|
||||
}
|
12
server/api/error.go
Normal file
12
server/api/error.go
Normal file
@ -0,0 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"docker4ssh/ssh"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type errorGetResponse APIError
|
||||
|
||||
func ErrorGet(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) {
|
||||
return APIError{Message: "Example error message"}, http.StatusBadRequest
|
||||
}
|
16
server/api/info.go
Normal file
16
server/api/info.go
Normal file
@ -0,0 +1,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"docker4ssh/ssh"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type infoGetResponse struct {
|
||||
ContainerID string `json:"container_id"`
|
||||
}
|
||||
|
||||
func InfoGet(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) {
|
||||
return infoGetResponse{
|
||||
ContainerID: user.Container.FullContainerID,
|
||||
}, http.StatusOK
|
||||
}
|
15
server/api/ping.go
Normal file
15
server/api/ping.go
Normal file
@ -0,0 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"docker4ssh/ssh"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type pingGetResponse struct {
|
||||
Received int64 `json:"received"`
|
||||
}
|
||||
|
||||
func PingGet(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) {
|
||||
return pingGetResponse{Received: time.Now().UnixNano()}, http.StatusOK
|
||||
}
|
35
server/api/utils.go
Normal file
35
server/api/utils.go
Normal file
@ -0,0 +1,35 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type APIError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func structJsonLookup(v interface{}) (map[string]reflect.Kind, error) {
|
||||
rt := reflect.TypeOf(v)
|
||||
if rt.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("given interface is not a struct")
|
||||
}
|
||||
|
||||
lookup := make(map[string]reflect.Kind)
|
||||
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
field := rt.Field(i)
|
||||
|
||||
name := strings.Split(field.Tag.Get("json"), ",")[0]
|
||||
value := field.Type.Kind()
|
||||
|
||||
if field.Type.Kind() == reflect.Struct {
|
||||
value = reflect.Map
|
||||
}
|
||||
|
||||
lookup[name] = value
|
||||
}
|
||||
|
||||
return lookup, nil
|
||||
}
|
Reference in New Issue
Block a user