Initial commit

This commit is contained in:
2021-12-19 17:30:51 +01:00
commit a589014106
65 changed files with 7437 additions and 0 deletions

123
server/api/api.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}