diff --git a/.github/workflows/create-release-workflow.yml b/.github/workflows/create-release-workflow.yml index b925b5ef..cae86c41 100644 --- a/.github/workflows/create-release-workflow.yml +++ b/.github/workflows/create-release-workflow.yml @@ -8,6 +8,8 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 - uses: actions/setup-go@v2 + - name: Install mingw-w64 + run: sudo apt-get update && sudo apt-get install gcc-multilib g++-multilib gcc-mingw-w64 g++-mingw-w64 - name: Get git tag id: tag_name uses: little-core-labs/get-git-tag@v3.0.2 diff --git a/.github/workflows/test-workflow.yml b/.github/workflows/test-workflow.yml index 0fa2d995..6d34f61a 100644 --- a/.github/workflows/test-workflow.yml +++ b/.github/workflows/test-workflow.yml @@ -60,9 +60,10 @@ jobs: docker-push: needs: [test-npm, test-go] runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + if: github.event_name == 'push' && github.ref != 'refs/heads/master' steps: - uses: actions/checkout@v2 + - uses: rlespinasse/github-slug-action@v3.x - uses: actions/setup-node@v1 - uses: actions/setup-go@v2 - uses: docker/setup-buildx-action@v1 @@ -78,4 +79,4 @@ jobs: context: ./docker/ file: ./docker/Dockerfile-local push: true - tags: ofsm/ofsm:develop + tags: ofsm/ofsm:${{ env.GITHUB_REF_SLUG }} diff --git a/.gitignore b/.gitignore index 159e67b9..379a7329 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ node_modules/ dev/ dev_packs/ -/factorio-server-manager* -/factorio_server_manager* +factorio-server-manager* +factorio_server_manager* auth.leveldb* conf.json *.exe @@ -18,3 +18,4 @@ mix-manifest.json /app/*.css* .vscode .env +*.db diff --git a/CHANGELOG.md b/CHANGELOG.md index 7286a536..331e03a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [Unreleased] + +## [0.10.0] - 2021-02-10 +### Added +- Config files can be defined with absolute paths. - Thanks to @FoxAmes +- Support for >= 1.1.14 factorio saves - Thanks to @knoxfighter +- Setting in `info.json` to allow usage without ssl/tls - Thanks to @knoxfighter + +### Changed +- Rework of the authentication, to have a bit more security. - Thanks to @knoxfighter +- Changed from leveldb to sqlite3 as backend database. - Thanks to @knoxfighter +- generate new random passwords, if no exist, or if they are "factorio". - Thanks to @knoxfighter +- Use "OpenFactorioServerManager" instead of "mroote" as go package name. - Thanks to @mroote +- Disable mods-page, while server is running - Thanks to @knoxfighter +- Renamed GO-package from `mroote` to `OpenFactorioServerManager` to match git repo - Thanks to @mroote + +### Fixed +- old factorio versions depended by mods always shown as compatible - Thanks to @knoxfighter +- Crosscompilation with mingw-w64 on linux. (Broke with sqlite3) - Thanks to @knoxfighter +- Crash on async writing to websocket room array. - Thanks to @knoxfighter + ## [0.9.0] - 2021-01-07 ### Added - Autostart factorio, when starting the server-manager - Thanks to @Psychomantis71 @@ -13,7 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Rework of the docker image, so it allows easy updating of factorio - Thanks to @ita-sammann ### Fixed -- Console page is now working correctly (directly reloading still bugged until new UI) - Thanks to @jannaahs +- Console page is now working correctly - Thanks to @jannaahs - Mod Search fixed by new implementation, which does not rely on the search endpoint of the mod portal - Thanks to @jannaahs - Listen on port 80, previously port 8080 was used. Can be changed with `--port ` - Update version numbers in Docker containers diff --git a/Makefile b/Makefile index 42f795c2..a64a5618 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ build/factorio-server-manager-%.zip: clean app/bundle factorio-server-manager-% @cp -r app/ factorio-server-manager/ @cp conf.json.example factorio-server-manager/conf.json @zip -r $@ factorio-server-manager > /dev/null + @rm -r factorio-server-manager/ app/bundle: @echo "Building Frontend" @@ -27,13 +28,13 @@ factorio-server-manager-linux: @echo "Building Backend - Linux" @mkdir -p factorio-server-manager @cd src; \ - GO111MODULE=on GOOS=linux GOARCH=amd64 go build -ldflags="-extldflags=-static" -o ../factorio-server-manager/factorio-server-manager . + CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -ldflags="-extldflags=-static" -o ../factorio-server-manager/factorio-server-manager . factorio-server-manager-windows: @echo "Building Backend - Windows" @mkdir -p factorio-server-manager @cd src; \ - GO111MODULE=on GOOS=windows GOARCH=386 go build -ldflags="-extldflags=-static" -o ../factorio-server-manager/factorio-server-manager.exe . + GO111MODULE=on GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CXX=x86_64-w64-mingw32-g++ CC=x86_64-w64-mingw32-gcc go build -ldflags="-extldflags=-static" -o ../factorio-server-manager/factorio-server-manager.exe . gen_release: build/factorio-server-manager-linux.zip build/factorio-server-manager-windows.zip @echo "Done" diff --git a/conf.json.example b/conf.json.example index 8e125211..27346aec 100644 --- a/conf.json.example +++ b/conf.json.example @@ -1,9 +1,7 @@ { - "username": "admin", - "password": "factorio", - "database_file": "auth.leveldb", - "cookie_encryption_key": "topsecretkey", - "settings_file": "server-settings.json", - "log_file": "factorio-server-manager.log", - "rcon_pass": "factorio_rcon" + "rcon_pass": "", + "sq_lite_database_file": "sqlite.db", + "cookie_encryption_key": "", + "settings_file": "server-settings.json", + "log_file": "factorio-server-manager.log" } diff --git a/docker/.env b/docker/.env index 547fb13e..d8d614a6 100644 --- a/docker/.env +++ b/docker/.env @@ -1,6 +1,3 @@ -ADMIN_USER=admin -ADMIN_PASS=factorio RCON_PASS= -COOKIE_ENCRYPTION_KEY= DOMAIN_NAME= EMAIL_ADDRESS= diff --git a/docker/Dockerfile b/docker/Dockerfile index edce5207..21ecb414 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,11 +2,8 @@ FROM frolvlad/alpine-glibc ENV FACTORIO_VERSION=stable \ - MANAGER_VERSION=0.9.0 \ - ADMIN_USER=admin \ - ADMIN_PASS=factorio \ - RCON_PASS="" \ - COOKIE_ENCRYPTION_KEY="" + MANAGER_VERSION=0.10.0 \ + RCON_PASS="" VOLUME /opt/fsm-data /opt/factorio/saves /opt/factorio/mods /opt/factorio/config diff --git a/docker/Dockerfile-build b/docker/Dockerfile-build index 72677b3c..be131bec 100644 --- a/docker/Dockerfile-build +++ b/docker/Dockerfile-build @@ -1,14 +1,14 @@ FROM alpine:latest as build -RUN apk add --no-cache git make musl-dev go nodejs npm zip +RUN apk add --no-cache git make musl-dev go nodejs npm zip mingw-w64-gcc -ENV FACTORIO_BRANCH=develop ENV GOROOT /usr/lib/go ENV GOPATH /go ENV PATH /go/bin:$PATH ENV FACTORIO_ROOT /go/src/factorio-server-manager -COPY build-release.sh /usr/local/bin/build-release.sh +COPY docker/build-release.sh /usr/local/bin/build-release.sh +COPY ./ $FACTORIO_ROOT RUN mkdir -p ${GOPATH}/bin RUN chmod u+x /usr/local/bin/build-release.sh @@ -16,6 +16,7 @@ RUN chmod u+x /usr/local/bin/build-release.sh WORKDIR $FACTORIO_ROOT VOLUME /build +VOLUME $FACTORIO_ROOT RUN ["/usr/local/bin/build-release.sh"] diff --git a/docker/Dockerfile-local b/docker/Dockerfile-local index 3bc17aa8..4213a791 100644 --- a/docker/Dockerfile-local +++ b/docker/Dockerfile-local @@ -2,10 +2,7 @@ FROM frolvlad/alpine-glibc ENV FACTORIO_VERSION=latest \ - ADMIN_USER=admin \ - ADMIN_PASS=factorio \ - RCON_PASS="" \ - COOKIE_ENCRYPTION_KEY="" + RCON_PASS="" VOLUME /opt/fsm-data /opt/factorio/saves /opt/factorio/mods /opt/factorio/config diff --git a/docker/README.md b/docker/README.md index 8b8191e3..da524074 100644 --- a/docker/README.md +++ b/docker/README.md @@ -9,13 +9,8 @@ and [Docker Compose](https://docs.docker.com/compose/install/) installed. Copy `docker-compose.yaml` and `.env` files from this repository to somewhere on your server. Edit values in the `.env` file: -* `ADMIN_USER` (default `admin`): Name of the default user created for FSM UI. -* `ADMIN_PASS` (default `factorio`): Default user password. \ - __Important:__ _For security reasons, please change the default user name and password. Never use the defaults._ * `RCON_PASS` (default empty string): Password for Factorio RCON (FSM uses it to communicate with the Factorio server). \ If left empty, a random password will be generated and saved on the first start of the server. You can see the password in `fsm-data/conf.json` file. -* `COOKIE_ENCRYPTION_KEY` (default empty string): The key used to encrypt auth cookie for FSM UI. \ - If left empty, a random key will be generated and saved on the first start of the server. You can see the key in `fsm-data/conf.json` file. * `DOMAIN_NAME` (must be set manually): The domain name where your FSM UI will be available. Must be set, so [Let's Encrypt](https://letsencrypt.org/) service can issue a valid HTTPS certificate for this domain. * `EMAIL_ADDRESS` (must be set manually): Your email address. Used only by Let's Encrypt service. @@ -31,7 +26,7 @@ docker-compose up -d ### Simple configuration without HTTPS -If you don't care about HTTPS and want to run just the Factorio Server Manager, or want to run it on local machine you can use `docker-compose.simple.yaml`. +If you don't care about HTTPS and want to run just the Factorio Server Manager, or want to run it on a local machine you can use `docker-compose.simple.yaml`. Ignore `DOMAIN_NAME` and `EMAIL_ADDREESS` variables in `.env` file and run ``` @@ -42,15 +37,16 @@ docker-compose -f docker-compose.simple.yaml up -d By default container will download the latest version of factorio. If you want to use specific version, you can change the value of `FACTORIO_VERSION=latest` variable in the `docker-compose.yaml` file. +Any version can be used. Using `latest` will download the newest beta version. Using `stable` will download the newest stable version. ## Accessing the application -Go to the domain specified in your `.env` file in your web browser. If running on localhost host access the application at http://localhost +Go to the domain specified in your `.env` file in your web browser. If running on localhost access the application at http://localhost ### First start -When container starts it begins to dowload Factorio headless server archive, and only after that Factorio Server Manager server starts. -So when Docker Compose writes +When container starts it begins to download Factorio headless server archive, and only after that Factorio Server Manager server starts. +So when docker-compose writes ``` Creating factorio-server-manager ... done ``` @@ -68,7 +64,7 @@ Users can be added and deleted on the settings page. ## Updating Factorio -For now you can't update/downgrade the Factorio version from the UI. +For now, you can't update/downgrade the Factorio version from the UI. You can however do this using docker images while sustaining your security settings and map/modfiles. @@ -80,17 +76,19 @@ After container starts, latest Factorio version will be downloaded and installed ## Security -Authentication is supported in the application but it is recommended to ensure access to the Factorio manager UI is accessible via VPN or internal network. +Authentication is supported in the application, but it is recommended to ensure access to the Factorio manager UI is accessible via VPN or internal network. ## Development For development purposes it also has the ability to create the docker image from local sourcecode. This is done by running `build.sh` in the `docker` directory. This will delete all old executables and the node_modules directory (runs `make build`). The created docker image will have the tag `factorio-server-manager:dev`. ### Creating release bundles -A Dockerfile-build file is included for creating the release bundles. +A Dockerfile-build file is included for creating the release bundles. Use Docker version 20 in order to use the BUILDKIT environment, some issues have been encountered with Docker version 19. To create the bundle build the Dockerfile-build file with the following command. The release bundles are output to the ./dist directory. + +Run this command from the root factorio-server-manager directory. ``` -DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile-build -t ofsm-build --target=output -o dist . +DOCKER_BUILDKIT=1 docker build --no-cache -f docker/Dockerfile-build -t ofsm-build --target=build -o dist . ``` ## For everyone who actually read this thing to the end diff --git a/docker/build-release.sh b/docker/build-release.sh index 0ad07318..93702765 100755 --- a/docker/build-release.sh +++ b/docker/build-release.sh @@ -1,7 +1,8 @@ #!/bin/sh -echo "Cloning ${FACTORIO_BRANCH}" -git clone -b ${FACTORIO_BRANCH} https://github.com/mroote/factorio-server-manager.git ${FACTORIO_ROOT} +go_version=$(go version) + +echo "Go Version: ${go_version}" echo "Creating build..." make gen_release echo "Copying build artifacts from ${PWD}" diff --git a/docker/docker-compose.simple.yaml b/docker/docker-compose.simple.yaml index 10fcde98..5460d097 100644 --- a/docker/docker-compose.simple.yaml +++ b/docker/docker-compose.simple.yaml @@ -6,10 +6,7 @@ services: restart: "unless-stopped" environment: - "FACTORIO_VERSION=latest" - - "ADMIN_USER" - - "ADMIN_PASS" - "RCON_PASS" - - "COOKIE_ENCRYPTION_KEY" ports: - "80:80" - "34197:34197/udp" @@ -18,3 +15,4 @@ services: - "./factorio-data/saves:/opt/factorio/saves" - "./factorio-data/mods:/opt/factorio/mods" - "./factorio-data/config:/opt/factorio/config" + - "./factorio-data/mod_packs:/opt/fsm/mod_packs" diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 130e18c5..130ba35e 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -6,15 +6,13 @@ services: restart: "unless-stopped" environment: - "FACTORIO_VERSION=latest" - - "ADMIN_USER" - - "ADMIN_PASS" - "RCON_PASS" - - "COOKIE_ENCRYPTION_KEY" volumes: - "./fsm-data:/opt/fsm-data" - "./factorio-data/saves:/opt/factorio/saves" - "./factorio-data/mods:/opt/factorio/mods" - "./factorio-data/config:/opt/factorio/config" + - "./factorio-data/mod_packs:/opt/fsm/mod_packs" labels: - "traefik.enable=true" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 547e07c1..328b25dc 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -3,28 +3,12 @@ init_config() { jq_cmd='.' - if [ -n $ADMIN_USER ]; then - jq_cmd="${jq_cmd} | .username = \"$ADMIN_USER\"" - echo "Admin username is '$ADMIN_USER'" + if [ -n "$RCON_PASS" ]; then + jq_cmd="${jq_cmd} | .rcon_pass = \"$RCON_PASS\"" + echo "Factorio rcon password is '$RCON_PASS'" fi - if [ -n $ADMIN_PASS ]; then - jq_cmd="${jq_cmd} | .password = \"$ADMIN_PASS\"" - echo "Admin password is '$ADMIN_PASS'" - fi - echo "IMPORTANT! Please create new user and delete default admin user ASAP." - - if [ -z $RCON_PASS ]; then - RCON_PASS="$(random_pass)" - fi - jq_cmd="${jq_cmd} | .rcon_pass = \"$RCON_PASS\"" - echo "Factorio rcon password is '$RCON_PASS'" - - if [ -z $COOKIE_ENCRYPTION_KEY ]; then - COOKIE_ENCRYPTION_KEY="$(random_pass)" - fi - jq_cmd="${jq_cmd} | .cookie_encryption_key = \"$COOKIE_ENCRYPTION_KEY\"" - jq_cmd="${jq_cmd} | .database_file = \"/opt/fsm-data/auth.leveldb\"" + jq_cmd="${jq_cmd} | .sq_lite_database_file = \"/opt/fsm-data/sqlite.db\"" jq_cmd="${jq_cmd} | .log_file = \"/opt/fsm-data/factorio-server-manager.log\"" jq "${jq_cmd}" /opt/fsm/conf.json >/opt/fsm-data/conf.json @@ -47,5 +31,5 @@ fi install_game -cd /opt/fsm && ./factorio-server-manager --conf /opt/fsm-data/conf.json --dir /opt/factorio -port 80 +cd /opt/fsm && ./factorio-server-manager --conf /opt/fsm-data/conf.json --dir /opt/factorio --port 80 diff --git a/src/.env.example b/src/.env.example index 752c7493..1607eaf4 100644 --- a/src/.env.example +++ b/src/.env.example @@ -1,5 +1,5 @@ factorio_username= factorio_password= -conf=../../conf.json.example +conf=../../conf.json mod_dir=dev mod_pack_dir=dev_pack \ No newline at end of file diff --git a/src/api/auth.go b/src/api/auth.go index 5180eeef..4dd5604c 100644 --- a/src/api/auth.go +++ b/src/api/auth.go @@ -1,121 +1,236 @@ package api import ( - "github.com/mroote/factorio-server-manager/bootstrap" + "encoding/base64" "log" - "os" - "sync" + "net/http" - "github.com/apexskier/httpauth" + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" + "github.com/gorilla/sessions" + "golang.org/x/crypto/bcrypt" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) -type AuthHTTP struct { - backend httpauth.LeveldbAuthBackend - aaa httpauth.Authorizer -} +type User bootstrap.User -type User struct { - Username string `json:"username"` - Password string `json:"password"` - Role string `json:"role"` - Email string `json:"email"` +type Auth struct { + db *gorm.DB } -var once sync.Once -var instantiated *AuthHTTP - -func GetAuth() *AuthHTTP { - once.Do(func() { - Auth := &AuthHTTP{} - config := bootstrap.GetConfig() - _ = Auth.CreateAuth(config.DatabaseFile, config.CookieEncryptionKey) - _ = Auth.CreateOrUpdateUser(config.Username, config.Password, "admin", "") - instantiated = Auth - }) - return instantiated -} +var ( + sessionStore *sessions.CookieStore + auth Auth +) -func (auth *AuthHTTP) CreateAuth(backendFile string, cookieKey string) error { +func SetupAuth() { var err error - os.Mkdir(backendFile, 0755) - auth.backend, err = httpauth.NewLeveldbAuthBackend(backendFile) + config := bootstrap.GetConfig() + + cookieEncryptionKey, err := base64.StdEncoding.DecodeString(config.CookieEncryptionKey) if err != nil { - log.Printf("Error creating Auth backend: %s", err) - return err + log.Printf("Error decoding base64 cookie encryption key: %s", err) + panic(err) + } + sessionStore = sessions.NewCookieStore(cookieEncryptionKey) + sessionStore.Options = &sessions.Options{ + Path: "/", + Secure: config.Secure, } - roles := make(map[string]httpauth.Role) - roles["user"] = 30 - roles["admin"] = 80 + auth.db, err = gorm.Open(sqlite.Open(config.SQLiteDatabaseFile), nil) + if err != nil { + log.Printf("Error opening sqlite or gorm database: %s", err) + panic(err) + } - auth.aaa, err = httpauth.NewAuthorizer(auth.backend, []byte(cookieKey), "user", roles) + err = auth.db.AutoMigrate(&User{}) if err != nil { - log.Printf("Error creating authorizer: %s", err) - return err + log.Printf("Error AutoMigrating gorm database: %s", err) + panic(err) } - return nil + var userCount int64 + auth.db.Model(&User{}).Count(&userCount) + + if userCount == 0 { + // no user created yet, create a default one + var password = bootstrap.GenerateRandomPassword() + + var user User + user.Username = "admin" + user.Password = password + user.Role = "admin" + + err := auth.addUser(user) + if err != nil { + log.Printf("Error adding admin user to db: %s", err) + panic(err) + } + + log.Println("Created default admin user. Please change it's password as soon as possible.") + log.Printf("Username: %s", user.Username) + log.Printf("Password: %s", password) + } } -func (auth *AuthHTTP) CreateOrUpdateUser(username, password, role, email string) error { - user := httpauth.UserData{Username: username, Role: role, Email: email} - err := auth.backend.SaveUser(user) +func (a *Auth) checkPassword(username, password string) error { + var user User + result := a.db.Where(&User{Username: username}).Take(&user) + if result.Error != nil { + log.Printf("Error reading user from database: %s", result.Error) + return result.Error + } + + decodedHashPw, err := base64.StdEncoding.DecodeString(user.Password) if err != nil { - log.Printf("Error saving user: %s", err) + log.Printf("Error decoding base64 password: %s", err) return err } - err = auth.aaa.Update(nil, nil, username, password, email) + err = bcrypt.CompareHashAndPassword(decodedHashPw, []byte(password)) if err != nil { - log.Printf("Error updating user: %s", err) + if err != bcrypt.ErrMismatchedHashAndPassword { + log.Printf("Unexpected error comparing hash and pw: %s", err) + } return err } - log.Printf("Created/Updated user: %s", user.Username) + // Password correct + return nil +} +func (a *Auth) deleteUser(username string) error { + result := a.db.Model(&User{}).Where(&User{Username: username}).Delete(&User{}) + if result.Error != nil { + log.Printf("Error deleting user from database: %s", result.Error) + return result.Error + } return nil } -func (auth *AuthHTTP) listUsers() ([]User, error) { - var userResponse []User - users, err := auth.backend.Users() - if err != nil { - log.Printf("Error list users: %s", err) - return nil, err +func (a *Auth) hasUser(username string) (bool, error) { + var count int64 + result := a.db.Model(&User{}).Where(&User{Username: username}).Count(&count) + if result.Error != nil { + log.Printf("Error checking if user exists in database: %s", result.Error) + return false, result.Error } + return count == 1, nil +} - for _, user := range users { - u := User{Username: user.Username, Role: user.Role, Email: user.Email} - userResponse = append(userResponse, u) +func (a *Auth) getUser(username string) (User, error) { + var user User + result := a.db.Model(&User{}).Where(&User{Username: username}).Take(&user) + if result.Error != nil { + log.Printf("Error reading user from database: %s", result.Error) + return User{}, result.Error } - log.Printf("listing users: %v found", len(users)) - return userResponse, nil + return user, nil } -func (auth *AuthHTTP) addUser(username, password, email, role string) error { - user := httpauth.UserData{Username: username, Hash: []byte(password), Email: email, Role: role} - err := auth.backend.SaveUser(user) - if err != nil { - log.Printf("Error creating user %v: %s", user, err) +func (a *Auth) listUsers() ([]User, error) { + var users []User + result := a.db.Find(&users) + if result.Error != nil { + log.Printf("Error listing all users in database: %s", result.Error) + return nil, result.Error } - err = auth.aaa.Update(nil, nil, username, password, email) + return users, nil +} + +func (a *Auth) addUser(user User) error { + // encrypt password + pwHash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) if err != nil { - log.Printf("Error saving user: %s", err) + log.Printf("Error generating bcrypt hash from password: %s", err) return err } - log.Printf("Added user: %v", user) + user.Password = base64.StdEncoding.EncodeToString(pwHash) + + // add user to db + result := a.db.Create(&user) + if result.Error != nil { + log.Printf("Error creating user in database: %s", result.Error) + return result.Error + } + return nil } -func (auth *AuthHTTP) removeUser(username string) error { - err := auth.backend.DeleteUser(username) +func (a *Auth) addUserWithHash(user User) error { + // add user to db + result := a.db.Create(&user) + if result.Error != nil { + log.Printf("Error creating user in database: %s", result.Error) + return result.Error + } + + return nil +} + +func (a *Auth) changePassword(username, password string) error { + var user User + result := a.db.Model(&User{}).Where(&User{Username: username}).Take(&user) + if result.Error != nil { + log.Printf("Error reading user from database: %s", result.Error) + return result.Error + } + + hashPW, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { - log.Printf("Could not delete user %s, error: %s", username, err) + log.Printf("Error generatig bcrypt hash from new password: %s", err) return err } + user.Password = base64.StdEncoding.EncodeToString(hashPW) + + result = a.db.Save(&user) + if result.Error != nil { + log.Printf("Error resaving user in database: %s", result.Error) + return result.Error + } + return nil } + +// middleware function, that will be called for every request, that has to be authorized +func AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session, err := sessionStore.Get(r, "authentication") + if err != nil { + if session != nil { + session.Options.MaxAge = -1 + err2 := session.Save(r, w) + if err2 != nil { + log.Printf("Error deleting cookie: %s", err2) + } + } + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + username, ok := session.Values["username"] + if !ok { + http.Error(w, "Could not read username from sessioncookie", http.StatusUnauthorized) + return + } + + hasUser, err := auth.hasUser(username.(string)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if hasUser { + next.ServeHTTP(w, r) + } else { + log.Printf("Unauthenticated request %s %s %s", r.Method, r.Host, r.RequestURI) + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + }) +} diff --git a/src/api/handlers.go b/src/api/handlers.go index d0c6786d..afc02143 100644 --- a/src/api/handlers.go +++ b/src/api/handlers.go @@ -4,8 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/mroote/factorio-server-manager/bootstrap" - "github.com/mroote/factorio-server-manager/factorio" "io" "io/ioutil" "log" @@ -16,6 +14,10 @@ import ( "sync" "time" + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" + "github.com/OpenFactorioServerManager/factorio-server-manager/factorio" + "github.com/gorilla/sessions" + "github.com/gorilla/mux" ) @@ -35,18 +37,46 @@ func WriteResponse(w http.ResponseWriter, data interface{}) { } } -func ReadRequestBody(w http.ResponseWriter, r *http.Request, resp *interface{}) (body []byte, err error) { +func ReadRequestBody(w http.ResponseWriter, r *http.Request) (body []byte, resp interface{}, err error) { if r.Body == nil { - *resp = fmt.Sprintf("%s: no request body", readHttpBodyError) - log.Println(*resp) + resp = fmt.Sprintf("%s: no request body", readHttpBodyError) + log.Println(resp) w.WriteHeader(http.StatusBadRequest) - return nil, errors.New("no request body") + err = errors.New("no request body") + return } body, err = ioutil.ReadAll(r.Body) if err != nil { - *resp = fmt.Sprintf("%s: %s", readHttpBodyError, err) - log.Println(*resp) + resp = fmt.Sprintf("%s: %s", readHttpBodyError, err) + log.Println(resp) + w.WriteHeader(http.StatusInternalServerError) + } + return +} + +func ReadSessionStore(w http.ResponseWriter, r *http.Request, name string) (session *sessions.Session, resp interface{}, err error) { + session, err = sessionStore.Get(r, name) + if err != nil { + resp = fmt.Sprintf("Error reading session cookie [%s]: %s", name, err) + log.Println(resp) + if session != nil { + session.Options.MaxAge = -1 + err2 := session.Save(r, w) + if err2 != nil { + log.Printf("Error deleting session cookie: %s", err2) + } + } + w.WriteHeader(http.StatusUnauthorized) + } + return +} + +func SaveSession(w http.ResponseWriter, r *http.Request, session *sessions.Session) (resp interface{}, err error) { + err = session.Save(r, w) + if err != nil { + resp = fmt.Sprintf("Error saving session cookie: %s", err) + log.Println(resp) w.WriteHeader(http.StatusInternalServerError) } return @@ -265,7 +295,7 @@ func StartServer(w http.ResponseWriter, r *http.Request) { log.Printf("Starting Factorio server.") - body, err := ReadRequestBody(w, r, &resp) + body, resp, err := ReadRequestBody(w, r) if err != nil { return } @@ -406,16 +436,17 @@ func FactorioVersion(w http.ResponseWriter, r *http.Request) { // Unmarshall the User object from the given bytearray // This function has side effects (it will write to resp and to w, in case of an error) -func UnmarshallUserJson(body []byte, resp *interface{}, w http.ResponseWriter) (user User, err error) { +func UnmarshallUserJson(body []byte, w http.ResponseWriter) (user User, resp interface{}, err error) { err = json.Unmarshal(body, &user) if err != nil { - *resp = fmt.Sprintf("Unable to parse the request body: %s", err) - log.Println(*resp) + resp = fmt.Sprintf("Unable to parse the request body: %s", err) + log.Println(resp) w.WriteHeader(http.StatusBadRequest) } return } +// Handler for the Login func LoginUser(w http.ResponseWriter, r *http.Request) { var err error var resp interface{} @@ -427,26 +458,42 @@ func LoginUser(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - body, err := ReadRequestBody(w, r, &resp) + body, resp, err := ReadRequestBody(w, r) if err != nil { return } - user, err := UnmarshallUserJson(body, &resp, w) + user, resp, err := UnmarshallUserJson(body, w) if err != nil { return } log.Printf("Logging in user: %s", user.Username) - Auth := GetAuth() - err = Auth.aaa.Login(w, r, user.Username, user.Password, "/") + + err = auth.checkPassword(user.Username, user.Password) if err != nil { - resp = fmt.Sprintf("Error loggin in user: %s, error: %s", user.Username, err) + resp = fmt.Sprintf("Password for user %s wrong", user.Username) log.Println(resp) + w.WriteHeader(http.StatusUnauthorized) + return + } + + session, resp, err := ReadSessionStore(w, r, "authentication") + if err != nil { + return + } + + session.Values["username"] = user.Username + + resp, err = SaveSession(w, r, session) + if err != nil { return } log.Printf("User: %s, logged in successfully", user.Username) + + user.Password = "" + resp = user } func LogoutUser(w http.ResponseWriter, r *http.Request) { @@ -458,10 +505,16 @@ func LogoutUser(w http.ResponseWriter, r *http.Request) { }() w.Header().Set("Content-Type", "application/json;charset=UTF-8") - Auth := GetAuth() - if err = Auth.aaa.Logout(w, r); err != nil { - log.Printf("Error logging out current user") - w.WriteHeader(http.StatusInternalServerError) + + session, resp, err := ReadSessionStore(w, r, "authentication") + if err != nil { + return + } + + delete(session.Values, "username") + + resp, err = SaveSession(w, r, session) + if err != nil { return } @@ -478,15 +531,24 @@ func GetCurrentLogin(w http.ResponseWriter, r *http.Request) { }() w.Header().Set("Content-Type", "application/json;charset=UTF-8") - Auth := GetAuth() - user, err := Auth.aaa.CurrentUser(w, r) + + session, resp, err := ReadSessionStore(w, r, "authentication") if err != nil { - resp = fmt.Sprintf("Error getting user status: %s, error: %s", user.Username, err) + return + } + + username := session.Values["username"].(string) + + user, err := auth.getUser(username) + if err != nil { + resp = fmt.Sprintf("Error getting user: %s", err) log.Println(resp) w.WriteHeader(http.StatusInternalServerError) return } + user.Password = "" + resp = user } @@ -498,8 +560,8 @@ func ListUsers(w http.ResponseWriter, r *http.Request) { }() w.Header().Set("Content-Type", "application/json;charset=UTF-8") - Auth := GetAuth() - users, err := Auth.listUsers() + + users, err := auth.listUsers() if err != nil { resp = fmt.Sprintf("Error listing users: %s", err) log.Println(resp) @@ -518,19 +580,17 @@ func AddUser(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - body, err := ReadRequestBody(w, r, &resp) + body, resp, err := ReadRequestBody(w, r) if err != nil { return } - log.Printf("Adding user: %v", string(body)) - - user, err := UnmarshallUserJson(body, &resp, w) + user, resp, err := UnmarshallUserJson(body, w) if err != nil { return } - Auth := GetAuth() - err = Auth.addUser(user.Username, user.Password, user.Email, user.Role) + + err = auth.addUser(user) if err != nil { resp = fmt.Sprintf("Error in adding user {%s}: %s", user.Username, err) log.Println(resp) @@ -550,17 +610,17 @@ func RemoveUser(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - body, err := ReadRequestBody(w, r, &resp) + body, resp, err := ReadRequestBody(w, r) if err != nil { return } - user, err := UnmarshallUserJson(body, &resp, w) + user, resp, err := UnmarshallUserJson(body, w) if err != nil { return } - Auth := GetAuth() - err = Auth.removeUser(user.Username) + + err = auth.deleteUser(user.Username) if err != nil { resp = fmt.Sprintf("Error in removing user {%s}, error: %s", user.Username, err) log.Println(resp) @@ -570,6 +630,70 @@ func RemoveUser(w http.ResponseWriter, r *http.Request) { resp = fmt.Sprintf("User: %s successfully removed.", user.Username) } +func ChangePassword(w http.ResponseWriter, r *http.Request) { + var resp interface{} + + defer func() { + WriteResponse(w, resp) + }() + + w.Header().Set("Content-Type", "application/json;charset=UTF-8") + + body, resp, err := ReadRequestBody(w, r) + if err != nil { + return + } + + var user struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` + NewPasswordConfirm string `json:"new_password_confirmation"` + } + err = json.Unmarshal(body, &user) + if err != nil { + resp = fmt.Sprintf("Unable to parse the request body: %s", err) + log.Println(resp) + w.WriteHeader(http.StatusBadRequest) + return + } + + // only allow to change its own password + // get username from session cookie + session, resp, err := ReadSessionStore(w, r, "authentication") + if err != nil { + return + } + + username := session.Values["username"].(string) + + // check if password for user is correct + err = auth.checkPassword(username, user.OldPassword) + if err != nil { + resp = fmt.Sprintf("Password for user %s wrong", username) + log.Println(resp) + w.WriteHeader(http.StatusUnauthorized) + return + } + + // only run, when confirmation correct + if user.NewPassword != user.NewPasswordConfirm { + resp = fmt.Sprintf("Password confirmation incorrect") + log.Println(resp) + w.WriteHeader(http.StatusBadRequest) + return + } + + err = auth.changePassword(username, user.NewPassword) + if err != nil { + resp = fmt.Sprintf("Error changing password: %s", err) + log.Println(resp) + w.WriteHeader(http.StatusInternalServerError) + return + } + + resp = true +} + // GetServerSettings returns JSON response of server-settings.json file func GetServerSettings(w http.ResponseWriter, r *http.Request) { var resp interface{} @@ -594,7 +718,7 @@ func UpdateServerSettings(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - body, err := ReadRequestBody(w, r, &resp) + body, resp, err := ReadRequestBody(w, r) if err != nil { return } @@ -628,7 +752,7 @@ func UpdateServerSettings(w http.ResponseWriter, r *http.Request) { return } config := bootstrap.GetConfig() - err = ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.SettingsFile), settings, 0644) + err = ioutil.WriteFile(config.SettingsFile, settings, 0644) if err != nil { resp = fmt.Sprintf("Failed to save server settings: %v\n", err) log.Println(resp) diff --git a/src/api/mod_modpack_handler.go b/src/api/mod_modpack_handler.go index 032e8987..e1d9f29f 100644 --- a/src/api/mod_modpack_handler.go +++ b/src/api/mod_modpack_handler.go @@ -4,49 +4,49 @@ import ( "archive/zip" "errors" "fmt" - "github.com/gorilla/mux" - "github.com/mroote/factorio-server-manager/bootstrap" - "github.com/mroote/factorio-server-manager/factorio" "io" "log" "net/http" "os" "path/filepath" + + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" + "github.com/OpenFactorioServerManager/factorio-server-manager/factorio" + "github.com/gorilla/mux" ) -func CheckModPackExists(modPackMap factorio.ModPackMap, modPackName string, w http.ResponseWriter, resp interface{}) error { +func CheckModPackExists(modPackMap factorio.ModPackMap, modPackName string, w http.ResponseWriter) (resp interface{}, err error) { exists := modPackMap.CheckModPackExists(modPackName) if !exists { resp = fmt.Sprintf("requested modPack {%s} does not exist", modPackName) log.Println(resp) w.WriteHeader(http.StatusNotFound) - return errors.New("requested modPack does not exist") + err = errors.New("requested modPack does not exist") } - return nil + return } -func CreateNewModPackMap(w http.ResponseWriter, resp *interface{}) (modPackMap factorio.ModPackMap, err error) { +func CreateNewModPackMap(w http.ResponseWriter) (modPackMap factorio.ModPackMap, resp interface{}, err error) { modPackMap, err = factorio.NewModPackMap() if err != nil { w.WriteHeader(http.StatusInternalServerError) - *resp = fmt.Sprintf("Error creating modpackmap aka. list of all modpacks files : %s", err) - log.Println(*resp) + resp = fmt.Sprintf("Error creating modpackmap aka. list of all modpacks files : %s", err) + log.Println(resp) } return } -func ReadModPackRequest(w http.ResponseWriter, r *http.Request, resp *interface{}) (err error, packMap factorio.ModPackMap, modPackName string) { +func ReadModPackRequest(w http.ResponseWriter, r *http.Request) (err error, packMap factorio.ModPackMap, modPackName string, resp interface{}) { vars := mux.Vars(r) modPackName = vars["modpack"] - packMap, err = CreateNewModPackMap(w, resp) + packMap, resp, err = CreateNewModPackMap(w) if err != nil { return } - if err = CheckModPackExists(packMap, modPackName, w, resp); err != nil { - return - } + resp, err = CheckModPackExists(packMap, modPackName, w) + return } @@ -64,7 +64,7 @@ func ModPackListHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - modPackMap, err := CreateNewModPackMap(w, &resp) + modPackMap, resp, err := CreateNewModPackMap(w) if err != nil { return } @@ -85,12 +85,12 @@ func ModPackCreateHandler(w http.ResponseWriter, r *http.Request) { var modPackStruct struct { Name string `json:"name"` } - err = ReadFromRequestBody(w, r, &resp, &modPackStruct) + resp, err = ReadFromRequestBody(w, r, &modPackStruct) if err != nil { return } - modPackMap, err := CreateNewModPackMap(w, &resp) + modPackMap, resp, err := CreateNewModPackMap(w) if err != nil { return } @@ -116,7 +116,9 @@ func ModPackDeleteHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp) + var modPackMap factorio.ModPackMap + var modPackName string + err, modPackMap, modPackName, resp = ReadModPackRequest(w, r) if err != nil { return } @@ -136,7 +138,7 @@ func ModPackDownloadHandler(w http.ResponseWriter, r *http.Request) { var err error var resp interface{} - err, _, modPackName := ReadModPackRequest(w, r, &resp) + err, _, modPackName, resp := ReadModPackRequest(w, r) if err != nil { w.Header().Set("Content-Type", "application/json;charset=UTF-8") WriteResponse(w, resp) @@ -204,7 +206,9 @@ func ModPackLoadHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp) + var modPackMap factorio.ModPackMap + var modPackName string + err, modPackMap, modPackName, resp = ReadModPackRequest(w, r) if err != nil { return } @@ -232,7 +236,7 @@ func ModPackModListHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp) + err, modPackMap, modPackName, resp := ReadModPackRequest(w, r) if err != nil { return } @@ -250,7 +254,7 @@ func ModPackModToggleHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - err, packMap, packName := ReadModPackRequest(w, r, &resp) + err, packMap, packName, resp := ReadModPackRequest(w, r) if err != nil { return } @@ -258,7 +262,7 @@ func ModPackModToggleHandler(w http.ResponseWriter, r *http.Request) { var modPackStruct struct { ModName string `json:"name"` } - ReadFromRequestBody(w, r, &resp, &modPackStruct) + resp, err = ReadFromRequestBody(w, r, &modPackStruct) if err != nil { return } @@ -282,7 +286,7 @@ func ModPackModDeleteHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - err, packMap, packName := ReadModPackRequest(w, r, &resp) + err, packMap, packName, resp := ReadModPackRequest(w, r) if err != nil { return } @@ -290,7 +294,7 @@ func ModPackModDeleteHandler(w http.ResponseWriter, r *http.Request) { var modPackStruct struct { Name string `json:"name"` } - err = ReadFromRequestBody(w, r, &resp, &modPackStruct) + resp, err = ReadFromRequestBody(w, r, &modPackStruct) if err != nil { return } @@ -322,12 +326,12 @@ func ModPackModUpdateHandler(w http.ResponseWriter, r *http.Request) { DownloadUrl string `json:"downloadUrl"` Filename string `json:"filename"` } - err = ReadFromRequestBody(w, r, &resp, &modPackStruct) + resp, err = ReadFromRequestBody(w, r, &modPackStruct) if err != nil { return } - err, packMap, packName := ReadModPackRequest(w, r, &resp) + err, packMap, packName, resp := ReadModPackRequest(w, r) if err != nil { return } @@ -366,7 +370,7 @@ func ModPackModDeleteAllHandler(w http.ResponseWriter, r *http.Request) { WriteResponse(w, resp) }() - err, packMap, packName := ReadModPackRequest(w, r, &resp) + err, packMap, packName, resp := ReadModPackRequest(w, r) if err != nil { return } @@ -409,7 +413,7 @@ func ModPackModUploadHandler(w http.ResponseWriter, r *http.Request) { } defer formFile.Close() - err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp) + err, modPackMap, modPackName, resp := ReadModPackRequest(w, r) err = modPackMap[modPackName].Mods.UploadMod(formFile, fileHeader) if err != nil { @@ -438,12 +442,12 @@ func ModPackModPortalInstallHandler(w http.ResponseWriter, r *http.Request) { Filename string `json:"fileName"` ModName string `json:"modName"` } - err = ReadFromRequestBody(w, r, &resp, &data) + resp, err = ReadFromRequestBody(w, r, &data) if err != nil { return } - err, packMap, packName := ReadModPackRequest(w, r, &resp) + err, packMap, packName, resp := ReadModPackRequest(w, r) if err != nil { return } @@ -475,12 +479,12 @@ func ModPackModPortalInstallMultipleHandler(w http.ResponseWriter, r *http.Reque Name string `json:"name"` Version factorio.Version `json:"version"` } - err = ReadFromRequestBody(w, r, &resp, &data) + resp, err = ReadFromRequestBody(w, r, &data) if err != nil { return } - err, packMap, packName := ReadModPackRequest(w, r, &resp) + err, packMap, packName, resp := ReadModPackRequest(w, r) if err != nil { return } diff --git a/src/api/mod_modpack_handler_test.go b/src/api/mod_modpack_handler_test.go index 82bff5a9..41695460 100644 --- a/src/api/mod_modpack_handler_test.go +++ b/src/api/mod_modpack_handler_test.go @@ -3,10 +3,6 @@ package api import ( "bytes" "encoding/json" - "github.com/gorilla/mux" - "github.com/mroote/factorio-server-manager/bootstrap" - "github.com/mroote/factorio-server-manager/factorio" - "github.com/stretchr/testify/assert" "io" "mime/multipart" "net/http" @@ -15,6 +11,11 @@ import ( "path/filepath" "strings" "testing" + + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" + "github.com/OpenFactorioServerManager/factorio-server-manager/factorio" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" ) func SetupModPacks(t *testing.T, empty bool, emptyMods bool) { diff --git a/src/api/mod_portal_handler.go b/src/api/mod_portal_handler.go index 950d619b..076db45e 100644 --- a/src/api/mod_portal_handler.go +++ b/src/api/mod_portal_handler.go @@ -2,10 +2,11 @@ package api import ( "fmt" - "github.com/gorilla/mux" - "github.com/mroote/factorio-server-manager/factorio" "log" "net/http" + + "github.com/OpenFactorioServerManager/factorio-server-manager/factorio" + "github.com/gorilla/mux" ) func ModPortalListModsHandler(w http.ResponseWriter, r *http.Request) { @@ -71,12 +72,12 @@ func ModPortalInstallHandler(w http.ResponseWriter, r *http.Request) { Filename string `json:"fileName"` ModName string `json:"modName"` } - err = ReadFromRequestBody(w, r, &resp, &data) + resp, err = ReadFromRequestBody(w, r, &data) if err != nil { return } - mods, err := CreateNewMods(w, &resp) + mods, resp, err := CreateNewMods(w) if err != nil { return } @@ -106,7 +107,7 @@ func ModPortalLoginHandler(w http.ResponseWriter, r *http.Request) { Username string `json:"username"` Password string `json:"password"` } - err = ReadFromRequestBody(w, r, &resp, &data) + resp, err = ReadFromRequestBody(w, r, &data) if err != nil { return } @@ -174,12 +175,12 @@ func ModPortalInstallMultipleHandler(w http.ResponseWriter, r *http.Request) { Name string `json:"name"` Version factorio.Version `json:"version"` } - err = ReadFromRequestBody(w, r, &resp, &data) + resp, err = ReadFromRequestBody(w, r, &data) if err != nil { return } - modList, err := CreateNewMods(w, &resp) + modList, resp, err := CreateNewMods(w) if err != nil { return } diff --git a/src/api/mods_handler.go b/src/api/mods_handler.go index 2d83ff98..c4bb0c7b 100644 --- a/src/api/mods_handler.go +++ b/src/api/mods_handler.go @@ -4,38 +4,39 @@ import ( "archive/zip" "encoding/json" "fmt" - "github.com/mroote/factorio-server-manager/bootstrap" - "github.com/mroote/factorio-server-manager/factorio" - "github.com/mroote/factorio-server-manager/lockfile" "io" "log" "net/http" "os" "path/filepath" + + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" + "github.com/OpenFactorioServerManager/factorio-server-manager/factorio" + "github.com/OpenFactorioServerManager/factorio-server-manager/lockfile" ) -func CreateNewMods(w http.ResponseWriter, resp *interface{}) (modList factorio.Mods, err error) { +func CreateNewMods(w http.ResponseWriter) (modList factorio.Mods, resp interface{}, err error) { config := bootstrap.GetConfig() modList, err = factorio.NewMods(config.FactorioModsDir) if err != nil { - *resp = fmt.Sprintf("Error creating mods object: %s", err) - log.Println(*resp) + resp = fmt.Sprintf("Error creating mods object: %s", err) + log.Println(resp) w.WriteHeader(http.StatusInternalServerError) } return } -func ReadFromRequestBody(w http.ResponseWriter, r *http.Request, resp *interface{}, data interface{}) (err error) { +func ReadFromRequestBody(w http.ResponseWriter, r *http.Request, data interface{}) (resp interface{}, err error) { //Get Data out of the request - body, err := ReadRequestBody(w, r, resp) + body, resp, err := ReadRequestBody(w, r) if err != nil { return } err = json.Unmarshal(body, data) if err != nil { - *resp = fmt.Sprintf("Error unmarshalling requested struct JSON: %s", err) - log.Println(*resp) + resp = fmt.Sprintf("Error unmarshalling requested struct JSON: %s", err) + log.Println(resp) w.WriteHeader(http.StatusBadRequest) return } @@ -53,7 +54,7 @@ func ListInstalledModsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json;charset=UTF-8") - modList, err := CreateNewMods(w, &resp) + modList, resp, err := CreateNewMods(w) if err != nil { return } @@ -75,12 +76,12 @@ func ModToggleHandler(w http.ResponseWriter, r *http.Request) { Name string `json:"name"` } - err = ReadFromRequestBody(w, r, &resp, &data) + resp, err = ReadFromRequestBody(w, r, &data) if err != nil { return } - mods, err := CreateNewMods(w, &resp) + mods, resp, err := CreateNewMods(w) if err != nil { return } @@ -109,12 +110,12 @@ func ModDeleteHandler(w http.ResponseWriter, r *http.Request) { } // Get Data out of the request - err = ReadFromRequestBody(w, r, &resp, &data) + resp, err = ReadFromRequestBody(w, r, &data) if err != nil { return } - modList, err := CreateNewMods(w, &resp) + modList, resp, err := CreateNewMods(w) if err != nil { return } @@ -169,12 +170,12 @@ func ModUpdateHandler(w http.ResponseWriter, r *http.Request) { Filename string `json:"fileName"` } - err = ReadFromRequestBody(w, r, &resp, &modData) + resp, err = ReadFromRequestBody(w, r, &modData) if err != nil { return } - mods, err := CreateNewMods(w, &resp) + mods, resp, err := CreateNewMods(w) if err != nil { return } @@ -219,7 +220,7 @@ func ModUploadHandler(w http.ResponseWriter, r *http.Request) { } defer formFile.Close() - mods, err := CreateNewMods(w, &resp) + mods, resp, err := CreateNewMods(w) if err != nil { return } @@ -310,15 +311,17 @@ func LoadModsFromSaveHandler(w http.ResponseWriter, r *http.Request) { var saveFileStruct struct { Name string `json:"saveFile"` } - err = ReadFromRequestBody(w, r, &resp, &saveFileStruct) + + resp, err = ReadFromRequestBody(w, r, &saveFileStruct) if err != nil { return } + config := bootstrap.GetConfig() path := filepath.Join(config.FactorioSavesDir, saveFileStruct.Name) - f, err := factorio.OpenArchiveFile(path, "level.dat") + + f, err := factorio.OpenArchiveFile(path, "level.dat", "level-init.dat") if err != nil { - w.WriteHeader(http.StatusInternalServerError) resp = fmt.Sprintf("cannot open save level file: %v", err) log.Println(resp) w.WriteHeader(http.StatusInternalServerError) diff --git a/src/api/mods_handler_test.go b/src/api/mods_handler_test.go index c28cfb19..51600b2a 100644 --- a/src/api/mods_handler_test.go +++ b/src/api/mods_handler_test.go @@ -2,12 +2,6 @@ package api import ( "bytes" - "github.com/gorilla/mux" - "github.com/joho/godotenv" - "github.com/mroote/factorio-server-manager/bootstrap" - "github.com/mroote/factorio-server-manager/factorio" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "io" "log" "mime/multipart" @@ -17,6 +11,13 @@ import ( "path/filepath" "strings" "testing" + + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" + "github.com/OpenFactorioServerManager/factorio-server-manager/factorio" + "github.com/gorilla/mux" + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { diff --git a/src/api/routes.go b/src/api/routes.go index 5d6938a1..0dbf0216 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -1,8 +1,8 @@ package api import ( - "github.com/mroote/factorio-server-manager/api/websocket" - "log" + "github.com/OpenFactorioServerManager/factorio-server-manager/api/websocket" + "github.com/OpenFactorioServerManager/factorio-server-manager/factorio" "net/http" "github.com/gorilla/mux" @@ -13,25 +13,56 @@ type Route struct { Method string Pattern string HandlerFunc http.HandlerFunc + ServerOff bool // Set to `true' if factorio server has to be turned off to call this } type Routes []Route +func ServerOffMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // only run if server is turned off + server := factorio.GetFactorioServer() + if server.GetRunning() { + http.Error(w, "factorio server still running", http.StatusLocked) + } else { + next.ServeHTTP(w, r) + } + return + }) +} + func NewRouter() *mux.Router { r := mux.NewRouter().StrictSlash(true) + // create subrouter for authenticated calls + sr := r.NewRoute().Subrouter() + sr.Use(AuthMiddleware) + // API subrouter // Serves all JSON REST handlers prefixed with /api s := r.PathPrefix("/api").Subrouter() + s.Use(AuthMiddleware) + + // use subrouter for calls, that run only, when server is turned off + so := s.NewRoute().Subrouter() + so.Use(ServerOffMiddleware) + + s.NewRoute().Subrouter() for _, route := range apiRoutes { - s.Methods(route.Method). + var router *mux.Router + if route.ServerOff { + router = so + } else { + router = s + } + router.Methods(route.Method). Path(route.Pattern). Name(route.Name). - Handler(AuthorizeHandler(route.HandlerFunc)) + Handler(route.HandlerFunc) } // The login handler does not check for authentication. - s.Path("/login"). + r.Path("/api/login"). Methods("POST"). Name("LoginUser"). HandlerFunc(LoginUser) @@ -40,16 +71,14 @@ func NewRouter() *mux.Router { // Clients connecting to /ws establish websocket connection by upgrading // HTTP session. // Ensure user is logged in with the AuthorizeHandler middleware - r.Path("/ws"). + sr.Path("/ws"). Methods("GET"). Name("Websocket"). Handler( - AuthorizeHandler( - http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - websocket.ServeWs(w, r) - }, - ), + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + websocket.ServeWs(w, r) + }, ), ) @@ -60,38 +89,39 @@ func NewRouter() *mux.Router { Methods("GET"). Name("Login"). Handler(http.StripPrefix("/login", http.FileServer(http.Dir("./app/")))) - r.Path("/saves"). + + sr.Path("/saves"). Methods("GET"). Name("Saves"). - Handler(AuthorizeHandler(http.StripPrefix("/saves", http.FileServer(http.Dir("./app/"))))) - r.Path("/mods"). + Handler(http.StripPrefix("/saves", http.FileServer(http.Dir("./app/")))) + sr.Path("/mods"). Methods("GET"). Name("Mods"). - Handler(AuthorizeHandler(http.StripPrefix("/mods", http.FileServer(http.Dir("./app/"))))) - r.Path("/server-settings"). + Handler(http.StripPrefix("/mods", http.FileServer(http.Dir("./app/")))) + sr.Path("/server-settings"). Methods("GET"). Name("Server settings"). - Handler(AuthorizeHandler(http.StripPrefix("/server-settings", http.FileServer(http.Dir("./app/"))))) - r.Path("/game-settings"). + Handler(http.StripPrefix("/server-settings", http.FileServer(http.Dir("./app/")))) + sr.Path("/game-settings"). Methods("GET"). Name("Game settings"). - Handler(AuthorizeHandler(http.StripPrefix("/game-settings", http.FileServer(http.Dir("./app/"))))) - r.Path("/console"). + Handler(http.StripPrefix("/game-settings", http.FileServer(http.Dir("./app/")))) + sr.Path("/console"). Methods("GET"). Name("Console"). - Handler(AuthorizeHandler(http.StripPrefix("/console", http.FileServer(http.Dir("./app/"))))) - r.Path("/logs"). + Handler(http.StripPrefix("/console", http.FileServer(http.Dir("./app/")))) + sr.Path("/logs"). Methods("GET"). Name("Logs"). - Handler(AuthorizeHandler(http.StripPrefix("/logs", http.FileServer(http.Dir("./app/"))))) - r.Path("/user-management"). + Handler(http.StripPrefix("/logs", http.FileServer(http.Dir("./app/")))) + sr.Path("/user-management"). Methods("GET"). Name("User management"). - Handler(AuthorizeHandler(http.StripPrefix("/user-management", http.FileServer(http.Dir("./app/"))))) - r.Path("/help"). + Handler(http.StripPrefix("/user-management", http.FileServer(http.Dir("./app/")))) + sr.Path("/help"). Methods("GET"). Name("Help"). - Handler(AuthorizeHandler(http.StripPrefix("/help", http.FileServer(http.Dir("./app/"))))) + Handler(http.StripPrefix("/help", http.FileServer(http.Dir("./app/")))) // catch all route r.PathPrefix("/"). @@ -102,20 +132,6 @@ func NewRouter() *mux.Router { return r } -// Middleware returns a http.HandlerFunc which authenticates the users request -// Redirects user to login page if no session is found -func AuthorizeHandler(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - Auth := GetAuth() - if err := Auth.aaa.Authorize(w, r, true); err != nil { - log.Printf("Unauthenticated request %s %s %s", r.Method, r.Host, r.RequestURI) - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } - h.ServeHTTP(w, r) - }) -} - // Defines all API REST endpoints // All routes are prefixed with /api var apiRoutes = Routes{ @@ -124,101 +140,127 @@ var apiRoutes = Routes{ "GET", "/saves/list", ListSaves, + false, }, { "DlSave", "GET", "/saves/dl/{save}", DLSave, + false, }, { "UploadSave", "POST", "/saves/upload", UploadSave, + false, }, { "RemoveSave", "GET", "/saves/rm/{save}", RemoveSave, + false, }, { "CreateSave", "GET", "/saves/create/{save}", CreateSaveHandler, + true, }, { "LoadModsFromSave", "POST", "/saves/mods", LoadModsFromSaveHandler, + true, }, { "LogTail", "GET", "/log/tail", LogTail, + false, }, { "LoadConfig", "GET", "/config", LoadConfig, + false, }, { "StartServer", "POST", "/server/start", StartServer, + true, }, { "StopServer", "GET", "/server/stop", StopServer, + false, }, { "KillServer", "GET", "/server/kill", KillServer, + false, }, { "RunningServer", "GET", "/server/status", CheckServer, + false, }, { "FactorioVersion", "GET", "/server/facVersion", FactorioVersion, + false, }, { "LogoutUser", "GET", "/logout", LogoutUser, + false, }, { "StatusUser", "GET", "/user/status", GetCurrentLogin, + false, }, { "ListUsers", "GET", "/user/list", ListUsers, + false, }, { "AddUser", "POST", "/user/add", AddUser, + false, }, { "RemoveUser", "POST", "/user/remove", RemoveUser, + false, + }, { + "ChangePassword", + "POST", + "/user/password", + ChangePassword, + false, }, { "GetServerSettings", "GET", "/settings", GetServerSettings, + false, }, { "UpdateServerSettings", "POST", "/settings/update", UpdateServerSettings, + false, }, // Mod Portal Stuff { @@ -226,36 +268,43 @@ var apiRoutes = Routes{ "GET", "/mods/portal/list", ModPortalListModsHandler, + false, }, { "ModPortalGetModInfo", "GET", "/mods/portal/info/{mod}", ModPortalModInfoHandler, + false, }, { "ModPortalInstallMod", "POST", "/mods/portal/install", ModPortalInstallHandler, + true, }, { "ModPortalLogin", "POST", "/mods/portal/login", ModPortalLoginHandler, + false, }, { "ModPortalLoginStatus", "GET", "/mods/portal/loginstatus", ModPortalLoginStatusHandler, + false, }, { "ModPortalLogout", "GET", "/mods/portal/logout", ModPortalLogoutHandler, + false, }, { "ModPortalInstallMultiple", "POST", "/mods/portal/install/multiple", ModPortalInstallMultipleHandler, + true, }, // Mods Stuff { @@ -263,36 +312,43 @@ var apiRoutes = Routes{ "GET", "/mods/list", ListInstalledModsHandler, + false, }, { "ToggleMod", "POST", "/mods/toggle", ModToggleHandler, + true, }, { "DeleteMod", "POST", "/mods/delete", ModDeleteHandler, + true, }, { "DeleteAllMods", "POST", "/mods/delete/all", ModDeleteAllHandler, + true, }, { "UpdateMod", "POST", "/mods/update", ModUpdateHandler, + true, }, { "UploadMod", "POST", "/mods/upload", ModUploadHandler, + true, }, { "DownloadMods", "GET", "/mods/download", ModDownloadHandler, + false, }, // Mod Packs { @@ -300,26 +356,31 @@ var apiRoutes = Routes{ "GET", "/mods/packs/list", ModPackListHandler, + false, }, { "ModPackCreate", "POST", "/mods/packs/create", ModPackCreateHandler, + false, }, { "ModPackDelete", "POST", "/mods/packs/{modpack}/delete", ModPackDeleteHandler, + false, }, { "ModPackDownload", "GET", "/mods/packs/{modpack}/download", ModPackDownloadHandler, + false, }, { "LoadModPack", "POST", "/mods/packs/{modpack}/load", ModPackLoadHandler, + true, }, // Mods inside Mod Packs { @@ -327,40 +388,48 @@ var apiRoutes = Routes{ "GET", "/mods/packs/{modpack}/list", ModPackModListHandler, + false, }, { "ModPackToggleMod", "POST", "/mods/packs/{modpack}/mod/toggle", ModPackModToggleHandler, + false, }, { "ModPackDeleteMod", "POST", "/mods/packs/{modpack}/mod/delete", ModPackModDeleteHandler, + false, }, { "ModPackDeleteAllMod", "POST", "/mods/packs/{modpack}/mod/delete/all", ModPackModDeleteAllHandler, + false, }, { "ModPackUpdateMod", "POST", "/mods/packs/{modpack}/mod/update", ModPackModUpdateHandler, + false, }, { "ModPackUploadMod", "POST", "/mods/packs/{modpack}/mod/upload", ModPackModUploadHandler, + false, }, { "ModPackModPortalInstallMod", "POST", "/mods/packs/{modpack}/portal/install", ModPackModPortalInstallHandler, + false, }, { "ModPackModPortalInstallMultiple", "POST", "/mods/packs/{modpack}/portal/install/multiple", ModPackModPortalInstallMultipleHandler, + false, }, } diff --git a/src/api/websocket/wshub.go b/src/api/websocket/wshub.go index 1b5eba19..9724c061 100644 --- a/src/api/websocket/wshub.go +++ b/src/api/websocket/wshub.go @@ -1,8 +1,9 @@ package websocket import ( - "github.com/mroote/factorio-server-manager/bootstrap" "reflect" + + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" ) // the hub, that is exported and can be used anywhere to work with the websocket @@ -39,9 +40,6 @@ type wsRoom struct { // same as the key of the map in the wsHub name string - // the wsHub this room is part of - hub *wsHub - // clients that are in this room. This list is a sublist of the one inside the hub clients map[*wsClient]bool @@ -155,7 +153,6 @@ func (hub *wsHub) GetRoom(name string) *wsRoom { } else { room := &wsRoom{ name: name, - hub: hub, clients: make(map[*wsClient]bool), register: make(chan *wsClient), unregister: make(chan *wsClient), @@ -187,11 +184,16 @@ func (room *wsRoom) run() { case client := <-room.unregister: if _, ok := room.clients[client]; ok { delete(room.clients, client) - if len(room.clients) == 0 { - // remove this room - delete(room.hub.rooms, room.name) - return - } + // FIXME when more rooms are used, remove empty rooms. + // Since we only have a few rooms at the same time, just keep them. + // This is code, that will cause a concurrent call on `wsHub.rooms`. + // To fix this, move the deletion into the hub. + // Be careful to think about race conditions, if a user registered to the room, before room was really deleted. + //if len(room.clients) == 0 { + // //remove this room + // delete(room.hub.rooms, room.name) + // return + //} } case message := <-room.send: for client := range room.clients { @@ -208,11 +210,6 @@ func (room *wsRoom) run() { LogCache = append(LogCache, message.Message.(string)) config := bootstrap.GetConfig() - // Set ConsoleCacheSize to 25 if not set! - if config.ConsoleCacheSize == 0 { - config.ConsoleCacheSize = 25 - } - // When cache is bigger than max size, delete one line if len(LogCache) > config.ConsoleCacheSize { LogCache = LogCache[1:] diff --git a/src/bootstrap/config.go b/src/bootstrap/config.go index 0dd84e84..24242022 100644 --- a/src/bootstrap/config.go +++ b/src/bootstrap/config.go @@ -1,8 +1,10 @@ package bootstrap import ( + "encoding/base64" "encoding/json" "fmt" + "github.com/gorilla/securecookie" "github.com/jessevdk/go-flags" "log" "math/rand" @@ -29,37 +31,42 @@ type Flags struct { } type Config struct { - FactorioDir string `json:"factorio_dir"` - FactorioSavesDir string `json:"saves_dir"` - FactorioModsDir string `json:"mods_dir"` - FactorioModPackDir string `json:"mod_pack_dir"` - FactorioConfigFile string `json:"config_file"` - FactorioConfigDir string `json:"config_directory"` - FactorioLog string `json:"logfile"` - FactorioBinary string `json:"factorio_binary"` - FactorioRconPort int `json:"rcon_port"` - FactorioRconPass string `json:"rcon_pass"` - FactorioCredentialsFile string `json:"factorio_credentials_file"` - FactorioIP string `json:"factorio_ip"` + FactorioDir string `json:"factorio_dir,omitempty"` + FactorioSavesDir string `json:"saves_dir,omitempty"` + FactorioBaseModDir string `json:"basemod_dir,omitempty"` + FactorioModsDir string `json:"mods_dir,omitempty"` + FactorioModPackDir string `json:"mod_pack_dir,omitempty"` + FactorioConfigFile string `json:"config_file,omitempty"` + FactorioConfigDir string `json:"config_directory,omitempty"` + FactorioLog string `json:"logfile,omitempty"` + FactorioBinary string `json:"factorio_binary,omitempty"` + FactorioRconPort int `json:"rcon_port,omitempty"` + FactorioRconPass string `json:"rcon_pass,omitempty"` + FactorioCredentialsFile string `json:"factorio_credentials_file,omitempty"` + FactorioIP string `json:"factorio_ip,omitempty"` FactorioAdminFile string `json:"-"` - ServerIP string `json:"server_ip"` - ServerPort string `json:"server_port"` - MaxUploadSize int64 `json:"max_upload_size"` - Username string `json:"username"` - Password string `json:"password"` - DatabaseFile string `json:"database_file"` - CookieEncryptionKey string `json:"cookie_encryption_key"` - SettingsFile string `json:"settings_file"` - LogFile string `json:"log_file"` - ConfFile string - GlibcCustom string - GlibcLocation string - GlibcLibLoc string - Autostart string - ConsoleCacheSize int `json:"console_cache_size"` // the amount of cached lines, inside the factorio output cache + ServerIP string `json:"server_ip,omitempty"` + ServerPort string `json:"server_port,omitempty"` + MaxUploadSize int64 `json:"max_upload_size,omitempty"` + DatabaseFile string `json:"database_file,omitempty"` + SQLiteDatabaseFile string `json:"sq_lite_database_file,omitempty"` + CookieEncryptionKey string `json:"cookie_encryption_key,omitempty"` + SettingsFile string `json:"settings_file,omitempty"` + LogFile string `json:"log_file,omitempty"` + ConfFile string `json:"-"` + GlibcCustom string `json:"-"` + GlibcLocation string `json:"-"` + GlibcLibLoc string `json:"-"` + Autostart string `json:"-"` + ConsoleCacheSize int `json:"console_cache_size,omitempty"` // the amount of cached lines, inside the factorio output cache + Secure bool `json:"secure"` // set to `false` to use this tool without SSL/TLS (Default: `true`) } -var instantiated Config +// set Configs default values. JSON unmarshal will replace when it found something different +var instantiated = Config{ + ConsoleCacheSize: 25, + Secure: true, +} func NewConfig(args []string) Config { var opts Flags @@ -67,7 +74,7 @@ func NewConfig(args []string) Config { if err != nil { failOnError(err, "Failed to parse arguments") } - instantiated = mapFlags(opts) + instantiated.mapFlags(opts) instantiated.loadServerConfig() abs, err := filepath.Abs(instantiated.FactorioModPackDir) @@ -80,17 +87,92 @@ func GetConfig() Config { return instantiated } +func (config *Config) updateConfigFile() { + file, err := os.OpenFile(config.ConfFile, os.O_RDONLY, 0) + failOnError(err, "Error opening file") + defer file.Close() + + var conf Config + decoder := json.NewDecoder(file) + decoder.Decode(&conf) + + err = file.Close() + failOnError(err, "Error closing json file") + + var resave bool + + // set cookie encryption key, if empty + // also set it, if the base64 string is not valid + _, base64Err := base64.StdEncoding.DecodeString(conf.CookieEncryptionKey) + if conf.CookieEncryptionKey == "" || conf.CookieEncryptionKey == "topsecretkey" || base64Err != nil { + log.Println("CookieEncryptionKey invalid or empty, create new random one") + randomKey := securecookie.GenerateRandomKey(32) + conf.CookieEncryptionKey = base64.StdEncoding.EncodeToString(randomKey) + + resave = true + } + + if conf.FactorioRconPass == "" || conf.FactorioRconPass == "factorio_rcon" { + // password is "factorio" .. change it + conf.FactorioRconPass = GenerateRandomPassword() + + log.Println("Rcon password default one or empty, generated new one:") + log.Printf("Password: %s", conf.FactorioRconPass) + + resave = true + } + + if conf.DatabaseFile != "" { + // Migrate leveldb to sqlite + // set new db name + // just rename the file from the old path + dbFileDir := filepath.Dir(conf.DatabaseFile) + conf.SQLiteDatabaseFile = filepath.Join(dbFileDir, "sqlite.db") + + MigrateLevelDBToSqlite(conf.DatabaseFile, conf.SQLiteDatabaseFile) + + // remove old db name + conf.DatabaseFile = "" + resave = true + } + + if resave { + // save json file again + file, err = os.OpenFile(config.ConfFile, os.O_WRONLY, 0) + failOnError(err, "Error opening file for writing") + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", "\t") + err = encoder.Encode(conf) + failOnError(err, "Error encoding JSON config file.") + } +} + // Loads server configuration files // JSON config file contains default values, // config file will overwrite any provided flags func (config *Config) loadServerConfig() { - file, err := os.Open(config.ConfFile) + // load and potentially update conf.json + config.updateConfigFile() + + file, err := os.OpenFile(config.ConfFile, os.O_RDWR, 0) failOnError(err, "Error loading config file.") + defer file.Close() decoder := json.NewDecoder(file) err = decoder.Decode(&config) failOnError(err, "Error decoding JSON config file.") + if !filepath.IsAbs(config.SettingsFile) { + config.SettingsFile = filepath.Join(config.FactorioConfigDir, config.SettingsFile) + } + + if config.FactorioBaseModDir == "" { + config.FactorioBaseModDir = filepath.Join(config.FactorioDir, "data", "base") + } + + // Set random port as rconPort config.FactorioRconPort = randomPort() } @@ -102,26 +184,29 @@ func randomPort() int { return rand.Intn(5000) + 40000 } -func mapFlags(flags Flags) Config { - var config = Config{ - Autostart: flags.Autostart, - GlibcCustom: flags.GlibcCustom, - GlibcLocation: flags.GlibcLocation, - GlibcLibLoc: flags.GlibcLibLoc, - ConfFile: flags.ConfFile, - FactorioDir: flags.FactorioDir, - ServerIP: flags.ServerIP, - ServerPort: flags.FactorioPort, - FactorioIP: flags.FactorioIP, - FactorioSavesDir: filepath.Join(flags.FactorioDir, "saves"), - FactorioModsDir: filepath.Join(flags.FactorioDir, "mods"), - FactorioModPackDir: flags.ModPackDir, - FactorioConfigDir: filepath.Join(flags.FactorioDir, "config"), - FactorioConfigFile: filepath.Join(flags.FactorioDir, flags.FactorioConfigFile), - FactorioBinary: filepath.Join(flags.FactorioDir, flags.FactorioBinary), - FactorioCredentialsFile: "./factorio.auth", - FactorioAdminFile: "server-adminlist.json", - MaxUploadSize: flags.FactorioMaxUpload, +func (config *Config) mapFlags(flags Flags) { + config.Autostart = flags.Autostart + config.GlibcCustom = flags.GlibcCustom + config.GlibcLocation = flags.GlibcLocation + config.GlibcLibLoc = flags.GlibcLibLoc + config.ConfFile = flags.ConfFile + config.FactorioDir = flags.FactorioDir + config.ServerIP = flags.ServerIP + config.ServerPort = flags.FactorioPort + config.FactorioIP = flags.FactorioIP + config.FactorioSavesDir = filepath.Join(flags.FactorioDir, "saves") + config.FactorioModsDir = filepath.Join(flags.FactorioDir, "mods") + config.FactorioModPackDir = flags.ModPackDir + config.FactorioConfigDir = filepath.Join(flags.FactorioDir, "config") + config.FactorioConfigFile = filepath.Join(flags.FactorioDir, flags.FactorioConfigFile) + config.FactorioCredentialsFile = "./factorio.auth" + config.FactorioAdminFile = "server-adminlist.json" + config.MaxUploadSize = flags.FactorioMaxUpload + + if filepath.IsAbs(flags.FactorioBinary) { + config.FactorioBinary = flags.FactorioBinary + } else { + config.FactorioBinary = filepath.Join(flags.FactorioDir, flags.FactorioBinary) } if runtime.GOOS == "windows" { @@ -130,8 +215,6 @@ func mapFlags(flags Flags) Config { } else { config.FactorioLog = filepath.Join(config.FactorioDir, "factorio-current.log") } - - return config } func failOnError(err error, msg string) { diff --git a/src/bootstrap/user.go b/src/bootstrap/user.go new file mode 100644 index 00000000..8e487e00 --- /dev/null +++ b/src/bootstrap/user.go @@ -0,0 +1,129 @@ +package bootstrap + +import ( + "encoding/base64" + "encoding/json" + "github.com/syndtr/goleveldb/leveldb" + "golang.org/x/crypto/bcrypt" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "log" + "math/rand" + "os" +) + +type User struct { + gorm.Model + Username string `json:"username",gorm:"uniqueIndex,not null"` + Password string `json:"password",gorm:"not null"` + Role string `json:"role",gorm:"not null"` + Email string `json:"email"` +} + +func MigrateLevelDBToSqlite(oldDBFile, newDBFile string) { + oldDB, err := leveldb.OpenFile(oldDBFile, nil) + if err != nil { + log.Printf("Error opening old leveldb: %s", err) + panic(err) + } + defer oldDB.Close() + + newDB, err := gorm.Open(sqlite.Open(newDBFile), nil) + if err != nil { + log.Printf("Error open sqlite and gorm: %s", err) + panic(err) + } + defer func() { + db, err2 := newDB.DB() + if err2 != nil { + log.Printf("Error getting real DB from gorm: %s", err2) + } + if db != nil { + err2 = db.Close() + if err2 != nil { + log.Printf("Error closing real DB of gorm: %s", err2) + panic(err2) + } + } + }() + + err = newDB.AutoMigrate(&User{}) + if err != nil { + log.Printf("Error autoMigrating sqlite database with user: %s", err) + panic(err) + } + + oldUserData, err := oldDB.Get([]byte("httpauth::userdata"), nil) + if err != nil { + log.Printf("Error getting `httpauth::userdata` from leveldb: %s", err) + panic(err) + } + + var migrationData map[string]struct { + Username string + Email string + Hash string + Role string + } + err = json.Unmarshal(oldUserData, &migrationData) + if err != nil { + log.Printf("Error unmarshalling old ") + panic(err) + } + + for _, datum := range migrationData { + // check if password is "factorio", which was the default password in the old system + decodedHash, err := base64.StdEncoding.DecodeString(datum.Hash) + if err != nil { + log.Printf("Error decoding base64 hash: %s", err) + panic(err) + } + + err = bcrypt.CompareHashAndPassword(decodedHash, []byte("factorio")) + if err == nil { + // password is "factorio" .. change it + newPassword := GenerateRandomPassword() + + bcryptPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + log.Printf("Error generating has from password: %s", err) + panic(err) + } + + datum.Hash = base64.StdEncoding.EncodeToString(bcryptPassword) + + log.Println(`Migrated user in database. It still had default password "factorio" set. New credentials:`) + log.Printf("Username: %s", datum.Username) + log.Printf("Password: %s", newPassword) + } + + user := &User{ + Username: datum.Username, + Password: datum.Hash, + Role: datum.Role, + Email: datum.Email, + } + + newDB.Create(user) + } + + oldDB.Close() + + // delete oldDB + log.Println("Deleting old leveldb database.") + err = os.RemoveAll(oldDBFile) + if err != nil { + log.Printf("Error removing leveldb: %s", err) + panic(err) + } +} + +var randLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func GenerateRandomPassword() string { + pass := make([]rune, 24) + for i := range pass { + pass[i] = randLetters[rand.Intn(len(randLetters))] + } + return string(pass) +} diff --git a/src/factorio/credentials.go b/src/factorio/credentials.go index ede9ad33..80055909 100644 --- a/src/factorio/credentials.go +++ b/src/factorio/credentials.go @@ -3,10 +3,11 @@ package factorio import ( "encoding/json" "errors" - "github.com/mroote/factorio-server-manager/bootstrap" "io/ioutil" "log" "os" + + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" ) type Credentials struct { diff --git a/src/factorio/gamelog.go b/src/factorio/gamelog.go index f30d6ef5..e920301e 100644 --- a/src/factorio/gamelog.go +++ b/src/factorio/gamelog.go @@ -1,9 +1,10 @@ package factorio import ( - "github.com/hpcloud/tail" - "github.com/mroote/factorio-server-manager/bootstrap" "log" + + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" + "github.com/hpcloud/tail" ) func TailLog() ([]string, error) { diff --git a/src/factorio/mod_Mods.go b/src/factorio/mod_Mods.go index e7191ef3..c9f32983 100644 --- a/src/factorio/mod_Mods.go +++ b/src/factorio/mod_Mods.go @@ -5,13 +5,14 @@ import ( "bytes" "errors" "fmt" - "github.com/mroote/factorio-server-manager/lockfile" "io" "io/ioutil" "log" "mime/multipart" "net/http" "path/filepath" + + "github.com/OpenFactorioServerManager/factorio-server-manager/lockfile" ) type Mods struct { diff --git a/src/factorio/mod_modInfo.go b/src/factorio/mod_modInfo.go index ee91426e..b9482771 100644 --- a/src/factorio/mod_modInfo.go +++ b/src/factorio/mod_modInfo.go @@ -4,13 +4,14 @@ import ( "archive/zip" "encoding/json" "errors" - "github.com/mroote/factorio-server-manager/lockfile" "io" "io/ioutil" "log" "os" "path/filepath" "strings" + + "github.com/OpenFactorioServerManager/factorio-server-manager/lockfile" ) type ModInfoList struct { @@ -111,7 +112,7 @@ func (modInfoList *ModInfoList) listInstalledMods() error { server := GetFactorioServer() // check both the factorio-version and the base mod dependency - modInfo.Compatibility = server.Version.GreaterC(modInfo.FactorioVersion) + modInfo.Compatibility = server.Version.GEC(modInfo.FactorioVersion) if modInfo.Compatibility && !base.Equals(NilVersion) { modInfo.Compatibility = server.Version.Compatible(base, op) } diff --git a/src/factorio/mod_modpack.go b/src/factorio/mod_modpack.go index d387ebad..2b10f5e2 100644 --- a/src/factorio/mod_modpack.go +++ b/src/factorio/mod_modpack.go @@ -2,12 +2,13 @@ package factorio import ( "errors" - "github.com/mroote/factorio-server-manager/bootstrap" "io" "io/ioutil" "log" "os" "path/filepath" + + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" ) type ModPackMap map[string]*ModPack diff --git a/src/factorio/mod_portal.go b/src/factorio/mod_portal.go index 385dc3aa..dd3d5deb 100644 --- a/src/factorio/mod_portal.go +++ b/src/factorio/mod_portal.go @@ -96,7 +96,7 @@ func ModPortalModDetails(modId string) (ModPortalStruct, error, int) { for key, release := range mod.Releases { requiredVersion = release.InfoJSON.FactorioVersion - release.Compatibility = installedBaseVersion.GreaterC(requiredVersion) + release.Compatibility = installedBaseVersion.Compatible(requiredVersion, ">=") mod.Releases[key] = release } diff --git a/src/factorio/mods.go b/src/factorio/mods.go index ebde7587..9fc4083e 100644 --- a/src/factorio/mods.go +++ b/src/factorio/mods.go @@ -5,12 +5,13 @@ import ( "bytes" "encoding/json" "errors" - "github.com/mroote/factorio-server-manager/bootstrap" "io/ioutil" "log" "os" "path/filepath" "strings" + + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" ) type LoginErrorResponse struct { diff --git a/src/factorio/rcon.go b/src/factorio/rcon.go index ba8f8f70..11d8edc4 100644 --- a/src/factorio/rcon.go +++ b/src/factorio/rcon.go @@ -1,11 +1,12 @@ package factorio import ( - "github.com/mroote/factorio-server-manager/bootstrap" "log" "strconv" - "github.com/majormjr/rcon" + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" + + "github.com/OpenFactorioServerManager/rcon" ) func connectRC() error { diff --git a/src/factorio/save.go b/src/factorio/save.go index 1bccdaa8..a97cd6e3 100644 --- a/src/factorio/save.go +++ b/src/factorio/save.go @@ -3,9 +3,9 @@ package factorio import ( "archive/zip" "encoding/binary" - "errors" "fmt" "io" + "os" ) type archiveFile struct { @@ -27,7 +27,8 @@ func (af *archiveFile) Close() error { return nil } -func OpenArchiveFile(path string, name string) (r io.ReadCloser, err error) { +// openNames contains a list of all. Will stop searching at the first occurance +func OpenArchiveFile(path string, openNames ...string) (r io.ReadCloser, err error) { archive, err := zip.OpenReader(path) if err != nil { return nil, err @@ -36,18 +37,21 @@ func OpenArchiveFile(path string, name string) (r io.ReadCloser, err error) { f := &archiveFile{archive: archive} for _, file := range archive.File { - if file.FileInfo().Name() == name { - f.ReadCloser, err = file.Open() - if err != nil { - archive.Close() - return nil, err + name := file.FileInfo().Name() + for _, openName := range openNames { + if name == openName { + f.ReadCloser, err = file.Open() + if err != nil { + archive.Close() + return nil, err + } + return f, nil } - return f, nil } } archive.Close() - return nil, errors.New("file not found") + return nil, os.ErrNotExist } type SaveHeader struct { @@ -100,7 +104,7 @@ func (h *SaveHeader) ReadFrom(r io.Reader) (err error) { } } - // campaign, example: "transport-belt-madness + // campaign, example: "transport-belt-madness" h.Campaign, err = readString(r, Version(h.FactorioVersion), false) if err != nil { return fmt.Errorf("read Campaign: %v", err) diff --git a/src/factorio/save_test.go b/src/factorio/save_test.go index e0aad97d..dbf2be24 100644 --- a/src/factorio/save_test.go +++ b/src/factorio/save_test.go @@ -4,6 +4,55 @@ import ( "testing" ) +// 1.1.14 changed the format of the saves, so new test has to be done +func Test1_1_14(t *testing.T) { + file, err := OpenArchiveFile("../factorio_testfiles/test_1_1_14.zip", "level-init.dat") + if err != nil { + t.Fatalf("Error opening level.datmetadata: %s", err) + } + defer file.Close() + + var header SaveHeader + err = header.ReadFrom(file) + if err != nil { + t.Fatalf("Error reading header: %s", err) + } + + testHeader := SaveHeader{ + FactorioVersion: Version{1, 1, 19, 0}, + Campaign: "transport-belt-madness", + Name: "level-01", + BaseMod: "base", + Difficulty: 1, + Finished: false, + PlayerWon: false, + NextLevel: "", + CanContinue: false, + FinishedButContinuing: false, + SavingReplay: false, + AllowNonAdminDebugOptions: true, + LoadedFrom: Version{1, 1, 19}, + LoadedFromBuild: 57957, + AllowedCommands: 1, + Mods: []Mod{ + { + Version: Version{1, 1, 19}, + Name: "base", + }, + { + Version: Version{3, 0, 0}, + Name: "belt-balancer", + }, + { + Version: Version{3, 0, 0}, + Name: "train-station-overview", + }, + }, + } + + header.Equals(testHeader, t) +} + // 1.1 Binary seems equal to 0.18/1.0 binary, just the default values changed func Test1_1(t *testing.T) { file, err := OpenArchiveFile("../factorio_testfiles/test_1_1.zip", "level.dat") diff --git a/src/factorio/saves.go b/src/factorio/saves.go index 8bbf92b1..01051b87 100644 --- a/src/factorio/saves.go +++ b/src/factorio/saves.go @@ -3,12 +3,13 @@ package factorio import ( "errors" "fmt" - "github.com/mroote/factorio-server-manager/bootstrap" "log" "os" "os/exec" "path/filepath" "time" + + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" ) type Save struct { diff --git a/src/factorio/server.go b/src/factorio/server.go index 703436fc..63920123 100644 --- a/src/factorio/server.go +++ b/src/factorio/server.go @@ -3,8 +3,7 @@ package factorio import ( "bufio" "encoding/json" - "github.com/mroote/factorio-server-manager/api/websocket" - "github.com/mroote/factorio-server-manager/bootstrap" + "errors" "io" "io/ioutil" "log" @@ -16,7 +15,9 @@ import ( "strings" "sync" - "github.com/majormjr/rcon" + "github.com/OpenFactorioServerManager/factorio-server-manager/api/websocket" + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" + "github.com/OpenFactorioServerManager/rcon" ) type Server struct { @@ -85,7 +86,7 @@ func NewFactorioServer() (err error) { return } - settingsPath := filepath.Join(config.FactorioConfigDir, config.SettingsFile) + settingsPath := config.SettingsFile var settings *os.File if _, err = os.Stat(settingsPath); os.IsNotExist(err) { @@ -165,7 +166,7 @@ func NewFactorioServer() (err error) { } //Load baseMod version - baseModInfoFile := filepath.Join(config.FactorioDir, "data", "base", "info.json") + baseModInfoFile := filepath.Join(config.FactorioBaseModDir, "info.json") bmifBa, err := ioutil.ReadFile(baseModInfoFile) if err != nil { log.Printf("couldn't open baseMods info.json: %s", err) @@ -226,7 +227,16 @@ func (server *Server) Run() error { if err != nil { log.Println("Failed to marshal FactorioServerSettings: ", err) } else { - ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.SettingsFile), data, 0644) + ioutil.WriteFile(config.SettingsFile, data, 0644) + } + + saves, err := ListSaves(config.FactorioSavesDir) + if err != nil { + log.Println("Failed to get saves list: ", err) + } + + if len(saves) == 0 { + return errors.New("No savefile exists on the server") } args := []string{} @@ -241,7 +251,7 @@ func (server *Server) Run() error { args = append(args, "--bind", server.BindIP, "--port", strconv.Itoa(server.Port), - "--server-settings", filepath.Join(config.FactorioConfigDir, config.SettingsFile), + "--server-settings", config.SettingsFile, "--rcon-port", strconv.Itoa(config.FactorioRconPort), "--rcon-password", config.FactorioRconPass) diff --git a/src/factorio/version.go b/src/factorio/version.go index a5155b4e..2c132ffb 100644 --- a/src/factorio/version.go +++ b/src/factorio/version.go @@ -73,7 +73,13 @@ func (v *Version) GreaterC(b Version) bool { return (v[0] == b[0] && v[1] == b[1] && (v[2] > b[2] || (v[2] == b[2] && v[3] > b[3]))) || (v[0] == 1 && b[0] == 0 && v[1] == 0 && b[1] == 18) } +// check greater equal of versions func (v Version) ge(b Version) bool { return v.Equals(b) || v.Greater(b) } + +// check greater equal of versions, with factorio incompatibility +func (v Version) GEC(b Version) bool { + return v.Equals(b) || v.GreaterC(b) +} func (v Version) le(b Version) bool { return v.Equals(b) || v.Less(b) } // Compatible returns true if the comparison between the two version operands is valid. diff --git a/src/factorio_testfiles/test_1_1_14.zip b/src/factorio_testfiles/test_1_1_14.zip new file mode 100644 index 00000000..56417ecb Binary files /dev/null and b/src/factorio_testfiles/test_1_1_14.zip differ diff --git a/src/go.mod b/src/go.mod index a02d32ab..6cefaf56 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,25 +1,25 @@ -module github.com/mroote/factorio-server-manager +module github.com/OpenFactorioServerManager/factorio-server-manager go 1.13 require ( - github.com/apexskier/httpauth v1.3.2 + github.com/OpenFactorioServerManager/rcon v0.0.0-20120923215419-8fbb8268b60a github.com/go-ini/ini v1.49.0 - github.com/go-sql-driver/mysql v1.4.1 // indirect + github.com/golang/protobuf v1.3.1 // indirect github.com/gorilla/mux v1.7.3 - github.com/gorilla/sessions v1.2.0 // indirect + github.com/gorilla/securecookie v1.1.1 + github.com/gorilla/sessions v1.2.1 github.com/gorilla/websocket v1.4.1 github.com/hpcloud/tail v1.0.0 github.com/jessevdk/go-flags v1.4.0 github.com/joho/godotenv v1.3.0 - github.com/lib/pq v1.2.0 // indirect - github.com/majormjr/rcon v0.0.0-20120923215419-8fbb8268b60a - github.com/mattn/go-sqlite3 v1.11.0 // indirect github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect github.com/stretchr/testify v1.6.1 - github.com/syndtr/goleveldb v1.0.0 // indirect - golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf // indirect - google.golang.org/appengine v1.6.5 // indirect + github.com/syndtr/goleveldb v1.0.0 + golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf + golang.org/x/net v0.0.0-20190603091049-60506f45cf65 // indirect + golang.org/x/text v0.3.2 // indirect gopkg.in/ini.v1 v1.49.0 // indirect - gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect + gorm.io/driver/sqlite v1.1.4 + gorm.io/gorm v1.20.11 ) diff --git a/src/go.sum b/src/go.sum index b7a836e1..75460a0e 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,13 +1,11 @@ -github.com/apexskier/httpauth v1.3.2 h1:PHwrq/eBRBLIrUthchpbDVTVR/ofBrj2LUcukCRhfXw= -github.com/apexskier/httpauth v1.3.2/go.mod h1:aEHd6x648VCocEK0vTsPKkjJ1sBPab3Z4V4MJs9YZAE= +github.com/OpenFactorioServerManager/rcon v0.0.0-20120923215419-8fbb8268b60a h1:BExxUM1IlQh7ba2D7ZTQ/aHe65YLI0GHgoPe1c3IIdA= +github.com/OpenFactorioServerManager/rcon v0.0.0-20120923215419-8fbb8268b60a/go.mod h1:ttMDbVLzmIWHIVGL6Wa+QG+hCzURs5gZDT12l5spIlg= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-ini/ini v1.49.0 h1:ymWFBUkwN3JFPjvjcJJ5TSTwh84M66QrH+8vOytLgRY= github.com/go-ini/ini v1.49.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= @@ -20,24 +18,24 @@ github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= -github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/majormjr/rcon v0.0.0-20120923215419-8fbb8268b60a h1:rXEd7/SA5sJvgl2zxM/nNblGa9kkpnx2phQATclw9Xk= -github.com/majormjr/rcon v0.0.0-20120923215419-8fbb8268b60a/go.mod h1:RNVV4T548mxgb643odZHF+pWh/YmGLxiKvtkbI1vRYE= -github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= -github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= +github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -75,19 +73,20 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.49.0 h1:MW0aLMiezbm/Ray0gJJ+nQFE2uOC9EpK2p5zPN3NqpM= gopkg.in/ini.v1 v1.49.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= -gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= +gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= +gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.11 h1:jYHQ0LLUViV85V8dM1TP9VBBkfzKTnuTXDjYObkI6yc= +gorm.io/gorm v1.20.11/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= diff --git a/src/main.go b/src/main.go index 79224a32..40c61432 100644 --- a/src/main.go +++ b/src/main.go @@ -1,12 +1,13 @@ package main import ( - "github.com/mroote/factorio-server-manager/api" - "github.com/mroote/factorio-server-manager/bootstrap" - "github.com/mroote/factorio-server-manager/factorio" "log" "net/http" "os" + + "github.com/OpenFactorioServerManager/factorio-server-manager/api" + "github.com/OpenFactorioServerManager/factorio-server-manager/bootstrap" + "github.com/OpenFactorioServerManager/factorio-server-manager/factorio" ) func main() { @@ -19,12 +20,12 @@ func main() { // Initialize Factorio Server struct err := factorio.NewFactorioServer() if err != nil { - log.Printf("Error occurred during Server initializaion: %v\n", err) + log.Printf("Error occurred during Server initialization: %v\n", err) return } // Initialize authentication system - api.GetAuth() + api.SetupAuth() // Initialize HTTP router -- also initializes websocket router := api.NewRouter() diff --git a/ui/App/App.jsx b/ui/App/App.jsx index 522597f9..9c1f0b0f 100644 --- a/ui/App/App.jsx +++ b/ui/App/App.jsx @@ -32,14 +32,12 @@ const App = () => { } } - const handleAuthenticationStatus = useCallback(async () => { - const status = await user.status(); - if (status?.Username) { + const handleAuthenticationStatus = useCallback(async (status) => { + if (status?.username) { setIsAuthenticated(true); - await updateServerStatus(); - + await updateServerStatus() socket.emit('server status subscribe'); - socket.on('server_status', updateServerStatus) + socket.on('server_status', updateServerStatus); } },[]); @@ -47,7 +45,6 @@ const App = () => { const loggedOut = await user.logout(); if (loggedOut) { setIsAuthenticated(false); - history.push('/login'); } }, []); diff --git a/ui/App/components/Error.jsx b/ui/App/components/Error.jsx new file mode 100644 index 00000000..75e6af94 --- /dev/null +++ b/ui/App/components/Error.jsx @@ -0,0 +1,14 @@ +import React from "react" + +const Error = ({error, message}) => { + if (error) { + return ( + + {message} + + ) + } + return null +} + +export default Error \ No newline at end of file diff --git a/ui/App/components/Flash.jsx b/ui/App/components/Flash.jsx index 131c0bda..cc777219 100644 --- a/ui/App/components/Flash.jsx +++ b/ui/App/components/Flash.jsx @@ -6,15 +6,21 @@ export const Flash = () => { let [message, setMessage] = useState(''); let [color, setColor] = useState(''); + let flashListener = ({message, color}) => { + setVisibility(true); + setMessage(message); + setColor(color); + setTimeout(() => { + setVisibility(false); + }, 4000); + } + useEffect(() => { - Bus.addListener('flash', ({message, color}) => { - setVisibility(true); - setMessage(message); - setColor(color); - setTimeout(() => { - setVisibility(false); - }, 4000); - }); + Bus.addListener('flash', flashListener); + + return function () { + Bus.removeListener('flash', flashListener); + } }, []); return ( diff --git a/ui/App/components/Input.jsx b/ui/App/components/Input.jsx index 44ce2452..998df12d 100644 --- a/ui/App/components/Input.jsx +++ b/ui/App/components/Input.jsx @@ -1,6 +1,17 @@ import React from "react"; -const Input = ({name, inputRef, placeholder = null, type="text", defaultValue=null, hasAutoComplete=true, onKeyDown=() => null}) => { +const Input = ({ + name, + inputRef, + placeholder = null, + type = "text", + defaultValue = null, + hasAutoComplete = true, + onKeyDown = () => null, + min = null, + value = undefined, + disabled = false + }) => { return ( ) } diff --git a/ui/App/components/Select.jsx b/ui/App/components/Select.jsx index f1ad16a8..7b3148fa 100644 --- a/ui/App/components/Select.jsx +++ b/ui/App/components/Select.jsx @@ -1,6 +1,9 @@ -import React from "react"; +import React, {useState} from "react"; + +const Select = ({name, inputRef, options, className = "", defaultValue = ""}) => { + + const [value, setValue] = useState(defaultValue); -const Select = ({name, inputRef, children, className}) => { return (
diff --git a/ui/App/views/Console.jsx b/ui/App/views/Console.jsx index 13a7075f..2909b749 100644 --- a/ui/App/views/Console.jsx +++ b/ui/App/views/Console.jsx @@ -1,6 +1,7 @@ import Panel from "../components/Panel"; import React, {useEffect, useRef, useState} from "react"; import socket from "../../api/socket"; +import Input from "../components/Input"; const Console = ({serverStatus}) => { @@ -33,14 +34,15 @@ const Console = ({serverStatus}) => {
    {logs?.map((log, i) => (
  • {log}
  • ))}
- { - if (e.key === "Enter" && socket) { - socket.emit("command send", consoleInput.current.value); - consoleInput.current.value = "" - } - } - }/> + { + if (e.key === "Enter" && socket) { + socket.emit("command send", consoleInput.current.value); + consoleInput.current.value = "" + } + }} + /> :

The console is not available, because Factorio is not running. diff --git a/ui/App/views/Controls.jsx b/ui/App/views/Controls.jsx index eb58199a..dca5a180 100644 --- a/ui/App/views/Controls.jsx +++ b/ui/App/views/Controls.jsx @@ -5,6 +5,8 @@ import server from "../../api/resources/server"; import savesResource from "../../api/resources/saves"; import {useForm} from "react-hook-form"; import Select from "../components/Select"; +import Input from "../components/Input"; +import Error from "../components/Error"; const Controls = ({serverStatus, updateServerStatus}) => { @@ -15,6 +17,10 @@ const Controls = ({serverStatus, updateServerStatus}) => { const { handleSubmit, register, errors } = useForm(); const startServer = async (data) => { + if(saves.length === 1 && saves[0].name === "Load Latest") { + window.flash("Save must be created before starting server", "red"); + return; + } await server.start(data.ip, parseInt(data.port), data.save); await updateServerStatus(); } @@ -73,26 +79,23 @@ const Controls = ({serverStatus, updateServerStatus}) => {

IP
- - {errors.ip && IP is required and must be valid.} +
Port
- - {errors.port && Port is required} +
Factorio Version
@@ -104,10 +107,12 @@ const Controls = ({serverStatus, updateServerStatus}) => { + defaultValue="Load Latest" + options={saves.map(save => new Object({ + value: save.name, + name: save.name + }))} + />
@@ -128,6 +133,6 @@ const Controls = ({serverStatus, updateServerStatus}) => { /> ) -} +}; export default Controls; \ No newline at end of file diff --git a/ui/App/views/Login.jsx b/ui/App/views/Login.jsx index c2f8f43a..c4b5392b 100644 --- a/ui/App/views/Login.jsx +++ b/ui/App/views/Login.jsx @@ -7,26 +7,33 @@ import Panel from "../components/Panel"; import Input from "../components/Input"; import Label from "../components/Label"; import {Flash} from "../components/Flash"; +import Error from "../components/Error"; const Login = ({handleLogin}) => { const {register, handleSubmit, errors} = useForm(); const history = useHistory(); - const location = useLocation() + const location = useLocation(); const onSubmit = async data => { - const loginAttempt = await user.login(data) - if (loginAttempt?.Username) { - await handleLogin(); - history.push('/'); + try { + const loginAttempt = await user.login(data) + if (loginAttempt?.username) { + await handleLogin(loginAttempt); + history.push('/'); + } + } catch (e) { + console.log(e); + window.flash("Login failed. Username or Password wrong.", "red"); + throw e; } }; // on mount check if user is authenticated useEffect(() => { (async () => { - const status = await user.status() - if (status?.Username) { - await handleLogin(); + const status = await user.status(); + if (status?.username) { + await handleLogin(status); history.push(location?.state?.from || '/'); } })(); @@ -41,7 +48,7 @@ const Login = ({handleLogin}) => {
diff --git a/ui/App/views/Mods/Mods.jsx b/ui/App/views/Mods/Mods.jsx index 35dff207..b7fa2000 100644 --- a/ui/App/views/Mods/Mods.jsx +++ b/ui/App/views/Mods/Mods.jsx @@ -13,7 +13,7 @@ import CreateModPack from "./components/CreateModPack"; import ModPack from "./components/ModPack"; import ModList from "./components/ModList"; -const Mods = () => { +const Mods = ({serverStatus}) => { const [installedMods, setInstalledMods] = useState([]); const [modPacks, setModPacks] = useState([]) @@ -103,31 +103,55 @@ const Mods = () => { .then(fetchInstalledMods) } + let disabled = serverStatus.status !== "stopped" + return (
- - - - - - - - - - - - + {disabled ? + + Changing mods is disabled while the server is running! +
+ } + /> + : + + + + + + + + + + + + } + } actions={ <> - - - Download all Mods + { + !disabled && + && + + } + Download all Mods } /> @@ -136,7 +160,16 @@ const Mods = () => { title="Mod packs" className="mb-6" content={ - modPacks.map((pack, i) => ) + modPacks.map( + (pack, i) => + + ) } actions={ diff --git a/ui/App/views/Mods/components/LoadMods.jsx b/ui/App/views/Mods/components/LoadMods.jsx index 77626681..2cef0dbe 100644 --- a/ui/App/views/Mods/components/LoadMods.jsx +++ b/ui/App/views/Mods/components/LoadMods.jsx @@ -35,9 +35,15 @@ const LoadMods = ({refreshMods}) => { return (
diff --git a/ui/App/views/Saves/components/UploadSaveForm.jsx b/ui/App/views/Saves/components/UploadSaveForm.jsx index 89eb10ce..034be606 100644 --- a/ui/App/views/Saves/components/UploadSaveForm.jsx +++ b/ui/App/views/Saves/components/UploadSaveForm.jsx @@ -2,6 +2,7 @@ import Button from "../../../components/Button"; import React, {useState} from "react"; import {useForm} from "react-hook-form"; import saves from "../../../../api/resources/saves"; +import Error from "../../../components/Error"; const UploadSaveForm = ({onSuccess}) => { @@ -30,8 +31,7 @@ const UploadSaveForm = ({onSuccess}) => { id="savefile" type="file"/>
{fileName}
- - {errors.savefile && Savefile is required} + diff --git a/ui/App/views/UserManagement/UserManagment.jsx b/ui/App/views/UserManagement/UserManagment.jsx index b18f73da..34829e52 100644 --- a/ui/App/views/UserManagement/UserManagment.jsx +++ b/ui/App/views/UserManagement/UserManagment.jsx @@ -2,6 +2,7 @@ import Panel from "../../components/Panel"; import React, {useCallback, useEffect, useState} from "react"; import user from "../../../api/resources/user"; import CreateUserForm from "./components/CreateUserForm"; +import ChangePasswordForm from "./components/ChangePasswordForm" import {faTrashAlt} from "@fortawesome/free-solid-svg-icons"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; @@ -55,9 +56,15 @@ const UserManagement = () => { } className="mb-4" /> + } + className="mb-4" + /> } + className="mb-4" /> ) diff --git a/ui/App/views/UserManagement/components/ChangePasswordForm.jsx b/ui/App/views/UserManagement/components/ChangePasswordForm.jsx new file mode 100644 index 00000000..78042fc7 --- /dev/null +++ b/ui/App/views/UserManagement/components/ChangePasswordForm.jsx @@ -0,0 +1,55 @@ +import {useForm} from "react-hook-form"; +import React from "react"; +import user from "../../../../api/resources/user"; +import Button from "../../../components/Button"; +import Label from "../../../components/Label"; +import Input from "../../../components/Input"; +import Error from "../../../components/Error"; + +const ChangePasswordForm = () => { + const {register, handleSubmit, errors, watch} = useForm(); + const password = watch('new_password'); + + const onSubmit = async (data) => { + const res = await user.changePassword(data); + if (res) { + // Update successful + window.flash("Password changed", "green") + } + } + + return ( +
+
+
+
+
+
+
+ +
+ ) +} + +export default ChangePasswordForm \ No newline at end of file diff --git a/ui/App/views/UserManagement/components/CreateUserForm.jsx b/ui/App/views/UserManagement/components/CreateUserForm.jsx index 0e8f33c0..a63010a9 100644 --- a/ui/App/views/UserManagement/components/CreateUserForm.jsx +++ b/ui/App/views/UserManagement/components/CreateUserForm.jsx @@ -2,6 +2,9 @@ import {useForm} from "react-hook-form"; import React from "react"; import user from "../../../../api/resources/user"; import Button from "../../../components/Button"; +import Label from "../../../components/Label"; +import Input from "../../../components/Input"; +import Error from "../../../components/Error"; const CreateUserForm = ({updateUserList}) => { @@ -9,7 +12,7 @@ const CreateUserForm = ({updateUserList}) => { const password = watch('password'); const onSubmit = async (data) => { - const res = user.add(data); + const res = await user.add(data); if (res) { updateUserList() } @@ -18,61 +21,53 @@ const CreateUserForm = ({updateUserList}) => { return (
- - + - {errors.username && Username is required} + type="text" + placeholder="Username" + /> +
- +
- - + - {errors.email && Email is required} + type="email" + placeholder="Email" + /> +
- - + - {errors.password && Password is required} + type="password" + placeholder="Password" + /> +
- - confirmation === password})} - id="password_confirmation" +
diff --git a/ui/api/resources/user.js b/ui/api/resources/user.js index 0c4f493f..5a2c884c 100644 --- a/ui/api/resources/user.js +++ b/ui/api/resources/user.js @@ -24,5 +24,9 @@ export default { delete: async (username) => { const response = await client.post('/api/user/remove', JSON.stringify({username})); return response.data; + }, + changePassword: async data => { + const response = await client.post('/api/user/password', data); + return response.data; } } \ No newline at end of file diff --git a/ui/api/socket.js b/ui/api/socket.js index 01ad28fa..14b6ea2c 100644 --- a/ui/api/socket.js +++ b/ui/api/socket.js @@ -1,69 +1,102 @@ import EventEmitter from "events"; +const bus = new EventEmitter(); + const ws_scheme = window.location.protocol === "https:" ? "wss" : "ws"; -const socket = new WebSocket(ws_scheme + "://" + window.location.host + "/ws"); -const bus = new EventEmitter(); +function connect() { + const socket = new WebSocket(ws_scheme + "://" + window.location.host + "/ws"); -bus.on('log subscribe', () => { - socket.send( - JSON.stringify( - { - room_name: "", - controls: { - type: "subscribe", - value: "gamelog" + function logSubscribeEvent() { + socket.send( + JSON.stringify( + { + room_name: "", + controls: { + type: "subscribe", + value: "gamelog" + } } - } - ) - ); -}); + ) + ); + } -bus.on('log unsubscribe', () => { - socket.send( - JSON.stringify( - { - room_name: "", - controls: { - type: "unsubscribe", - value: "gamelog" + function logUnsubscribeEvent() { + socket.send( + JSON.stringify( + { + room_name: "", + controls: { + type: "unsubscribe", + value: "gamelog" + } } - } - ) - ); -}) + ) + ); + } -bus.on('server status subscribe', () => { - socket.send( - JSON.stringify( - { - room_name: "", - controls: { - type: "subscribe", - value: "server_status" + function serverStatusSubscribeEvent() { + socket.send( + JSON.stringify( + { + room_name: "", + controls: { + type: "subscribe", + value: "server_status" + } } - } - ) - ); -}); + ) + ); + } -bus.on('command send', command => { - socket.send( - JSON.stringify( - { - room_name: "", - controls: { - type: "command", - value: command + function commandSendEvent(command) { + socket.send( + JSON.stringify( + { + room_name: "", + controls: { + type: "command", + value: command + } } - } - ) - ); -}); + ) + ); + } + + function registerEventEmitter() { + bus.on('log subscribe', logSubscribeEvent); + bus.on('log unsubscribe', logUnsubscribeEvent); + bus.on('server status subscribe', serverStatusSubscribeEvent); + bus.on('command send', commandSendEvent); + } + + function unregisterEventEmitter() { + bus.off('log subscribe', logSubscribeEvent); + bus.off('log unsubscribe', logUnsubscribeEvent); + bus.off('server status subscribe', serverStatusSubscribeEvent); + bus.off('command send', commandSendEvent); + } -socket.onmessage = e => { - const {room_name, message} = JSON.parse(e.data); - bus.emit(room_name, message); + socket.onmessage = e => { + const {room_name, message} = JSON.parse(e.data); + bus.emit(room_name, message); + } + + socket.onerror = e => { + socket.close(); + } + + socket.onclose = e => { + unregisterEventEmitter() + // reconnect after 5 seconds + setTimeout(connect, 5000); + } + + socket.onopen = e => { + registerEventEmitter(socket) + } } +connect(); + export default bus; \ No newline at end of file