Merge branch 'develop' into docker-fix

This commit is contained in:
Knuspel 2020-12-25 21:02:48 +01:00 committed by GitHub
commit f1405e827c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
182 changed files with 16551 additions and 12805 deletions

View File

@ -0,0 +1,62 @@
on:
release:
types: [published]
jobs:
add-assets-to-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-go@v2
- name: Get git tag
id: tag_name
uses: little-core-labs/get-git-tag@v3.0.2
- name: Get release
id: get_release
uses: bruceadams/get-release@v1.2.2
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Create release bundles
run: make gen_release
- name: Upload release (windows)
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./build/factorio-server-manager-windows.zip
asset_name: factorio-server-manager-windows-${{ steps.tag_name.outputs.tag }}.zip
asset_content_type: application/octet-stream
- name: Upload release (linux)
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: ./build/factorio-server-manager-linux.zip
asset_name: factorio-server-manager-linux-${{ steps.tag_name.outputs.tag }}.zip
asset_content_type: application/octet-stream
docker-push:
needs: ['add-assets-to-release']
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-go@v2
- uses: docker/setup-buildx-action@v1
- name: Get git tag
id: tag_name
uses: little-core-labs/get-git-tag@v3.0.2
- name: Login to dockerhub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: make build; cp build/factorio-server-manager-linux.zip docker/factorio-server-manager-linux.zip
- uses: docker/build-push-action@v2
with:
context: ./docker/
file: ./docker/Dockerfile-local
push: true
tags: ofsm/ofsm:latest,ofsm/ofsm:${{ steps.tag_name.outputs.tag }}

81
.github/workflows/test-workflow.yml vendored Normal file
View File

@ -0,0 +1,81 @@
on:
push:
branches:
- '**'
tags-ignore:
- '*.*'
pull_request:
branches:
- '**'
env:
factorio_password: ${{ secrets.FACTORIO_PASSWORD }}
factorio_username: ${{ secrets.FACTORIO_USERNAME }}
mod_dir: 'dev'
mod_pack_dir: 'dev_pack'
dir: '../'
conf: '../../conf.json.example'
jobs:
test-npm:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- run: make app/bundle
test-go:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
go: ['1.14', '1.15', '1']
runs-on: ${{ matrix.os }}
name: test-go ${{ matrix.go }} (${{ matrix.os }})
steps:
- uses: actions/checkout@v2
- name: Setup go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- if: contains(matrix.os, 'ubuntu')
run: |
cd src
if [[ -z "$factorio_password" ]]; then
echo "run only short tests"
go test ./... -v -test.short
else
echo "run full test suit"
go test ./... -v
fi
- if: contains(matrix.os, 'windows')
run: |
cd src
if ([Environment]::GetEnvironmentVariable('factorio_password', 'Machine')) {
echo "run full test suit"
go test ./... -v
} else {
echo "run only short tests"
go test ./... -v "-test.short"
}
docker-push:
needs: [test-npm, test-go]
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-go@v2
- uses: docker/setup-buildx-action@v1
- name: Login to dockerhub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: make build; cp build/factorio-server-manager-linux.zip docker/factorio-server-manager-linux.zip
- id: docker_build
uses: docker/build-push-action@v2
with:
context: ./docker/
file: ./docker/Dockerfile-local
push: true
tags: ofsm/ofsm:develop

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
node_modules/
dev/
dev_packs/
/factorio-server-manager*
/factorio_server_manager*
auth.leveldb*
@ -9,7 +10,6 @@ build/
/mod_packs/*
npm-debug.log
.idea/
/package-lock.json
factorio.auth
/pkg/
mix-manifest.json
@ -17,3 +17,4 @@ mix-manifest.json
/app/*.js*
/app/*.css*
.vscode
.env

View File

@ -1,40 +0,0 @@
language: go
go:
- 1.11.x
- 1.12.x
- 1.13.x
- 1.x
os:
- linux
- windows
env:
- GO111MODULE=on
script:
- cd src/
- go test -v
jobs:
include:
- stage: deploy
go: 1.x
os: linux
before_install:
- curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash -
- sudo apt-get install -y nodejs
script:
- make gen_release
- mv build/factorio-server-manager-linux.zip ~/factorio-server-manager-linux-${TRAVIS_TAG}.zip
- mv build/factorio-server-manager-windows.zip ~/factorio-server-manager-windows-${TRAVIS_TAG}.zip
deploy:
provider: releases
api_key: "${GITHUB_TOKEN}"
draft: true
skip_cleanup: true
on:
tags: true
file:
- ~/factorio-server-manager-linux-${TRAVIS_TAG}.zip
- ~/factorio-server-manager-windows-${TRAVIS_TAG}.zip

159
CHANGELOG.md Normal file
View File

@ -0,0 +1,159 @@
# Changelog
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]
### Added
- Autostart factorio, when starting the server-manager - Thanks to @Psychomantis71
### Changed
- Complete rework of the UI - Thanks to @jannaahs
- Backend is refactored and improved - Thanks to @knoxfighter and @jannaahs
- 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
- 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 <port>`
## [0.8.2] - 2020-01-08
Many bugfixes and a few small features in this release.
- Adds a flag for a custom glibc version, required on some distros such as CentOS
- bugfixes with file handling
- UI fixes and improvements
- CI bug fixes and build improvements
- and more bugfixes
Special thanks to @knoxfighter for all the contributions.
### Added
- Support for 0.17 server-adminlist.json
- Support for custom glibc location (RHEL/CENTOS)
### Changed
- Use bootstrap-fileinputs for savefile upload
- Login-Page uses bootstrap 4
### Fixed
- Login Page Design
- Sweetalert2 API changes
- allow_commands not misinterpreted as boolean anymore
- Fixed some filepaths on windows
- Fixed hardcoded Settings Path
- Fixed Upgrading, Removing Mods on Windows results in error
## [0.8.1] - 2019-03-01
### Fixed
- Fixed redirect, when not logged in
- Fixed login page completely white
## [0.8.0] - 2019-02-27
This release contains many bug fixes and features. Thanks to @knoxfighter @sean-callahan for the contributions!
- Fixes error in Factorio 0.17 saves
- Refactors and various bug fixes
## [0.7.5] - 2018-08-08
## Fixed
- fixes crash when mods have no basemodversion defined
## [0.7.4] - 2018-08-04
- Ability to auto download mods used in a save file courtesy @knoxfighter
- Fix bug in mod logging courtesy @c0nnex
## [0.7.3] - 2018-06-02
- Fixes fields in the settings dialog unable to be set to false. Courtesy @winadam.
- Various bugfixes in the mod settings page regarding version compatability. Courtesy @knoxfighter.
## [0.7.2] - 2018-05-02
### Fixed
- Fixes an error when searching in the mod portal.
## [0.7.1] - 2018-02-11
### Fixed
- Fixes an error in the configuration form where some fields were not editable.
## [0.7.0] - 2018-01-21
- Rewritten mods section now supporting installing mods directly from the Factorio mod portal and many other features courtesy @knoxfighter
- Various bug fixes
## [0.6.1] - 2017-12-22
- Adds the ability to specify the IP address for the Factorio game server to bind too.
- Updates the --rcon-password flag
- Small fixes
## [0.6.0] - 2017-01-25
This release adds a console feature using rcon to send commands and chat from the management interface.
## [0.5.2] - 2016-11-01
This release moves the server-settings.json config file. It will now save the file in the factorio/config directory.
## [0.5.1] - 2016-10-31
- Fixed bug where server-settings.json file is overwritten with default settings
- Started adding UI for editing the server-settings.json file
## [0.5.0] - 2016-10-11
- This release adds beta support for Windows users.
- Various updates for Factorio 0.14 are also included.
## [0.4.3] - 2016-09-15
This release has some small bug fixes in order to support Factorio server 0.14.
- Made the --latency-ms optional as it is removed in version 0.14
- Improved some error handling messages when starting the server.
## [0.4.2] - 2016-07-13
This release fixes a bug with Factorio 0.13 where the full path for save files must be specified when launching the server.
## [0.4.1] - 2016-05-15
This release fixes a bug where the UI reports an error when the Factorio Server was successfully started.
## [0.4.0] - 2016-05-15
### New features
- Abillity to create modpacks for easy distribution of mods
- Multiple users are now supported, create and delete users in the settings menu
### Features
- Allows control of the Factorio Server, starting and stopping the Factorio binary.
- Allows the management of save files, upload, download and delete saves.
- Manage installed mods, upload new ones, delete uneeded mods. Enable or disable individual mods.
- Allow viewing of the server logs and current configuration.
- Authentication for protecting against unauthorized users
- Available as a Docker container
- Abillity to create modpacks for easy distribution of mods
- Multiple users are now supported, create and delete users in the settings menu
## [0.3.1] - 2016-05-03
### Fixed
Fixes bug in #24 where Docker container cannot find conf.json file.
## [0.3.0] - 2016-05-01
### New features
- This release adds an authentication feature in Factorio Server Manager.
- Now able to be installed as a Docker container.
- Admin user credentials are configured in the conf.json file included in the release zip file.
### Features
- Allows control of the Factorio Server, starting and stopping the Factorio binary.
- Allows the management of save files, upload, download and delete saves.
- Manage installed mods, upload new ones, delete uneeded mods. Enable or disable individual mods.
- Allow viewing of the server logs and current configuration.
- Authentication for protecting against unauthorized users
- Available as a Docker container
## [0.2.0] - 2016-04-27
This release adds the ability to control the Factorio server. Allows stopping and starting of the server binary with advanced options.
### Features
- Allows control of the Factorio Server, starting and stopping the Factorio binary.
- Allows the management of save files, upload, download and delete saves.
- Manage installed mods, upload new ones, delete uneeded mods. Enable or disable individual mods.
- Allow viewing of the server logs and current configuration.
## [0.1.0] - 2016-04-25
First release of Factorio Server Manager
### Features
- Managing save files, create, download, delete saves
- Managing installed mods
- Factorio log tailing
- Factorio server configuration viewing

View File

@ -27,13 +27,13 @@ factorio-server-manager-linux:
@echo "Building Backend - Linux"
@mkdir -p factorio-server-manager
@cd src; \
GOOS=linux GOARCH=amd64 go build -o ../factorio-server-manager/factorio-server-manager .
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; \
GOOS=windows GOARCH=386 go build -o ../factorio-server-manager/factorio-server-manager.exe .
GO111MODULE=on GOOS=windows GOARCH=386 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"
@ -47,6 +47,6 @@ clean:
@-rm app/style.css.map
@-rm -r app/fonts/vendor/
@-rm -r app/images/vendor/
@-rm -r node_modules/
@-rm -rf node_modules/
@-rm -r pkg/
@-rm -r factorio-server-manager

View File

@ -1,4 +1,4 @@
[![Build Status](https://travis-ci.org/mroote/factorio-server-manager.svg?branch=master)](https://travis-ci.org/mroote/factorio-server-manager)
![.github/workflows/test-workflow.yml](https://github.com/OpenFactorioServerManager/factorio-server-manager/workflows/.github/workflows/test-workflow.yml/badge.svg)
# Factorio Server Manager
@ -17,17 +17,17 @@ This tool runs on a Factorio server and allows management of the Factorio server
## Installation Docker
1. Pull the Docker container from Docker Hub using the pull command
```
docker pull majormjr/factorio-server-manager
docker pull ofsm/ofsm:latest
```
2. Now you can start the container by running:
```
docker run --name factorio-manager -d -p 80:80 -p 443:443 -p 34197:34197/udp majormjr/factorio-server-manager
docker run --name ofsm -d -p 80:80 -p 34197:34197/udp ofsm/ofsm:latest
```
## Installation Linux
1. Download the latest release
* [https://github.com/mroote/factorio-server-manager/releases](https://github.com/mroote/factorio-server-manager/releases)
* [https://github.com/OpenFactorioServerManager/factorio-server-manager/releases](https://github.com/OpenFactorioServerManager/factorio-server-manager/releases)
2. Download the Factorio Standalone server and install to a known directory.
3. Run the server binary file, use the --dir flag to point the management server to your Factorio installation. If you are using the steam installation, point FSM to the steam directory.
* ```./factorio-server-manager --dir /home/user/.factorio ```
@ -36,7 +36,7 @@ This tool runs on a Factorio server and allows management of the Factorio server
## Installation Windows
1. Download the latest release
* [https://github.com/mroote/factorio-server-manager/releases](https://github.com/mroote/factorio-server-manager/releases)
* [https://github.com/OpenFactorioServerManager/factorio-server-manager/releases](https://github.com/OpenFactorioServerManager/factorio-server-manager/releases)
2. Download the Factorio Standalone server and install to a known directory.
3. Run the server binary file via cmd or Powershell, use the --dir flag to point the management server to your Factorio installation.
* ```.\factorio-server-manager --dir C:/Users/username/Factorio```
@ -66,7 +66,10 @@ Usage of ./factorio-server-manager:
Path to the glibc ld.so file (default "/opt/glibc-2.18/lib/ld-2.18.so")
-glibc-lib-loc
Path to the glibc lib folder (default "/opt/glibc-2.18/lib")
-autostart
Autostarts Factorio Server when FSM is starting. Default false [true/false]
(If no IP and/or port provided at startup, it will bind the factorio server to all interfaces
and set the server port to the default 34197, always loads latest save)
Example:
./factorio-server-manager --dir /home/user/.factorio --host 10.0.0.1
@ -78,16 +81,13 @@ Custom glibc example:
```
## Manage Factorio Server
![Factorio Server Manager Screenshot](http://i.imgur.com/q7tbzdH.png "Factorio Server Manager")
![Factorio Server Manager Screenshot](screenshots/Screenshot_Controls.png)
## Manage save files
![Factorio Server Manager Screenshot](http://i.imgur.com/M7kBAhI.png "Factorio Server Manager")
![Factorio Server Manager Screenshot](screenshots/Screenshot_Saves.png)
## Manage mods
![Factorio Server Manager Screenshot](https://imgur.com/QIb0Kr4.png "Factorio Server Manager")
## Manage modpacks
![Factorio Server Manager Screenshot](https://imgur.com/O701fB8.png "Factorio Server Manager")
![Factorio Server Manager Screenshot](screenshots/Screenshot_Mods.png)
@ -110,19 +110,19 @@ curl --cookie cookie.txt --insecure https://localhost/api/server/status
#### Requirements
+ Go 1.11
+ NodeJS
+ NodeJS >10.13.0
#### Building Releases
Creates a release zip for windows and linux: (this will install the dependencies listed in gopkgdeps)
```
git clone https://github.com/mroote/factorio-server-manager.git
git clone git@github.com:OpenFactorioServerManager/factorio-server-manager.git
cd factorio-server-manager
make gen_release
```
#### Building a Testing Binary:
```
git clone https://github.com/mroote/factorio-server-manager.git
git clone git@github.com:OpenFactorioServerManager/factorio-server-manager.git
cd factorio-server-manager
make
./factorio-server-manager/factorio-server-manager
@ -152,7 +152,7 @@ In every of those cases, also images and fonts will be copied to the app-folder.
### Building for Windows
1. Download the latest release source zip file
* [https://github.com/mroote/factorio-server-manager/releases](https://github.com/mroote/factorio-server-manager/releases)
* [https://github.com/OpenFactorioServerManager/factorio-server-manager/releases](https://github.com/OpenFactorioServerManager/factorio-server-manager/releases)
2. Unzip the Factorio Standalone server and move it to a known directory.
3. Download and install Go 1.11 or newer. https://golang.org/dl/
4. Download and install NodeJS 64-bit or 32-bit depending on your operating system, most users need 64-bit nowadays.
@ -185,15 +185,18 @@ go build
1. Fork it!
2. Create your feature branch: `git checkout -b my-new-feature`
3. Commit your changes: `git commit -am 'Add some feature'`
4. Add your changes a in human readable way into CHANGELOG.md
4. Push to the branch: `git push origin my-new-feature`
5. Submit a pull request :D
## Authors
* **Mitch Roote** - [roote.ca](https://roote.ca)
* **[knoxfighter](https://github.com/knoxfighter)**
* **[Jannaahs](https://github.com/jannaahs)**
## Special Thanks
- **[All Contributions](https://github.com/mroote/factorio-server-manager/graphs/contributors)**
- **[All Contributions](https://github.com/OpenFactorioServerManager/factorio-server-manager/graphs/contributors)**
- **mickael9** for reverseengineering the factorio-save-file: https://forums.factorio.com/viewtopic.php?f=5&t=8568#
## License

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -4,15 +4,14 @@
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Factorio Server Manager</title>
<link rel="icon" type="image/png" href="./images/favicon.ico">
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<link rel="stylesheet" type="text/css" href="./style.css">
</head>
<body class="hold-transition skin-blue sidebar-mini">
<body class="bg-black">
<div id="app"></div>
<div id="modal-root"></div>
<!-- Main application -->
<script src="./bundle.js"></script>
</body>

2
docker/.dockerignore Normal file
View File

@ -0,0 +1,2 @@
/fsm-data
/factorio-data

6
docker/.env Normal file
View File

@ -0,0 +1,6 @@
ADMIN_USER=admin
ADMIN_PASS=factorio
RCON_PASS=
COOKIE_ENCRYPTION_KEY=
DOMAIN_NAME=<YOUR DOMAIN NAME>
EMAIL_ADDRESS=<YOUR EMAIL ADDRESS>

2
docker/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/fsm-data
/factorio-data

View File

@ -1,29 +1,38 @@
# glibc is required for Factorio Server binaries to run
# Glibc is required for Factorio Server binaries to run
FROM frolvlad/alpine-glibc
ENV FACTORIO_VERSION=stable \
MANAGER_VERSION=0.8.2 \
ADMIN_PASSWORD=factorio
ADMIN_USER=admin \
ADMIN_PASS=factorio \
RCON_PASS="" \
COOKIE_ENCRYPTION_KEY=""
VOLUME /opt/factorio/saves /opt/factorio/mods /opt/factorio/config /security
VOLUME /opt/fsm-data /opt/factorio/saves /opt/factorio/mods /opt/factorio/config
RUN apk add --no-cache curl tar unzip nginx openssl xz
EXPOSE 80/tcp 34197/udp
WORKDIR /opt/
RUN apk add --no-cache curl tar xz unzip jq
WORKDIR /opt
# Install Factorio
RUN curl -s -L -S -k https://www.factorio.com/get-download/$FACTORIO_VERSION/headless/linux64 -o /tmp/factorio_$FACTORIO_VERSION.tar.xz && \
tar Jxf /tmp/factorio_$FACTORIO_VERSION.tar.xz && \
rm /tmp/factorio_$FACTORIO_VERSION.tar.xz && \
curl -sLSk https://github.com/OpenFactorioServerManager/factorio-server-manager/releases/download/$MANAGER_VERSION/factorio-server-manager-linux-$MANAGER_VERSION.zip \
--cacert /opt/github.pem -o /tmp/factorio-server-manager-linux_$MANAGER_VERSION.zip && \
unzip -qq /tmp/factorio-server-manager-linux_$MANAGER_VERSION.zip && \
rm /tmp/factorio-server-manager-linux_$MANAGER_VERSION.zip && \
# Install FSM
RUN curl --location "https://github.com/OpenFactorioServerManager/factorio-server-manager/releases/download/$MANAGER_VERSION/factorio-server-manager-linux-$MANAGER_VERSION.zip" \
--output /tmp/factorio-server-manager-linux_${MANAGER_VERSION}.zip \
&& unzip /tmp/factorio-server-manager-linux_${MANAGER_VERSION}.zip \
&& rm /tmp/factorio-server-manager-linux_${MANAGER_VERSION}.zip \
&& mv factorio-server-manager fsm
# Setup Nginx
mkdir -p /run/nginx && \
chown nginx:root /var/lib/nginx
COPY "init.sh" "/opt/init.sh"
COPY "nginx.conf" "/etc/nginx/nginx.conf"
EXPOSE 80/tcp 443/tcp 34190-34200/udp
COPY ./entrypoint.sh /opt/entrypoint.sh
ENTRYPOINT ["/opt/init.sh"]
ENTRYPOINT ["/opt/entrypoint.sh"]

View File

@ -1,20 +1,23 @@
FROM alpine:latest
FROM alpine:latest as build
RUN apk add --no-cache git make musl-dev go nodejs npm zip
ENV FAC_BRANCH=develop
ENV FACTORIO_BRANCH=develop
ENV GOROOT /usr/lib/go
ENV GOPATH /go
ENV PATH /go/bin:$PATH
ENV FAC_ROOT /go/src/factorio-server-manager
ENV FACTORIO_ROOT /go/src/factorio-server-manager
COPY build.sh /usr/local/bin/build.sh
COPY build-release.sh /usr/local/bin/build-release.sh
RUN mkdir -p ${GOPATH}/bin
RUN chmod u+x /usr/local/bin/build.sh
RUN chmod u+x /usr/local/bin/build-release.sh
WORKDIR $FAC_ROOT
WORKDIR $FACTORIO_ROOT
VOLUME /build
CMD ["/usr/local/bin/build.sh"]
RUN ["/usr/local/bin/build-release.sh"]
FROM scratch as output
COPY --from=build ${FACTORIO_ROOT}/build/* .

26
docker/Dockerfile-local Normal file
View File

@ -0,0 +1,26 @@
# Glibc is required for Factorio Server binaries to run
FROM frolvlad/alpine-glibc
ENV FACTORIO_VERSION=latest \
ADMIN_USER=admin \
ADMIN_PASS=factorio \
RCON_PASS="" \
COOKIE_ENCRYPTION_KEY=""
VOLUME /opt/fsm-data /opt/factorio/saves /opt/factorio/mods /opt/factorio/config
EXPOSE 80/tcp 34197/udp
RUN apk add --no-cache curl tar xz unzip jq
WORKDIR /opt
# Install FSM
COPY factorio-server-manager-linux.zip /factorio-server-manager-linux.zip
RUN unzip /factorio-server-manager-linux.zip \
&& rm /factorio-server-manager-linux.zip \
&& mv factorio-server-manager fsm
COPY entrypoint.sh /opt
ENTRYPOINT ["/opt/entrypoint.sh"]

View File

@ -1,27 +0,0 @@
# Variables can be overridden by setting environment variables
FACTORIO_PATH ?= ~/.factorio
SECURITY_PATH ?= $(FACTORIO_PATH)/security
SAVES_PATH ?= $(FACTORIO_PATH)/saves
MODS_PATH ?= $(FACTORIO_PATH)/mods
PORT_FORWARD ?= -p 80:80 -p 443:443 -p 34197:34197/udp
FACTORIO_BRANCH ?= develop
build:
docker build --build-arg FAC_BRANCH=$FACTORIO_BRANCH -f Dockerfile-build -t fsm-build .
docker build -t factorio-server-manager .
logs:
docker logs factorio-server -f
run:
docker run -d --name factorio-server -v $(SECURITY_PATH):/security -v $(SAVES_PATH):/opt/factorio/saves -v $(MODS_PATH):/opt/factorio/mods $(PORT_FORWARD) factorio-server-manager
stop:
docker stop factorio-server
docker rm factorio-server
clean:
docker rmi factorio-server-manager
docker stop fsm-build
docker rmi fsm-build

View File

@ -1,40 +1,64 @@
# Factorio Server Manager Docker Image
## Prerequisites
You need to have [Docker](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04)
and [Docker Compose](https://docs.docker.com/compose/install/) installed.
## Getting started?
Pull the Docker container from Docker Hub using the pull command
Copy `docker-compose.yaml` and `.env` files from this repository to somewhere on your server.
```
docker pull majormjr/factorio-server-manager
```
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.
Alternatively you can ignore `.env` file and edit this values directly in `environment` section of `docker-compose.yaml`.
But remember that if `.env` file is present, values set there take precedence over values set in `docker-compose.yaml`.
Now you can start the container by running:
```
docker run --name factorio-manager -d \
-p 80:80 \
-p 443:443 \
-p 34197:34197/udp \
majormjr/factorio-server-manager
docker-compose up -d
```
If you want persistent data in your container also mount the data volumes when starting:
### 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`.
Ignore `DOMAIN_NAME` and `EMAIL_ADDREESS` variables in `.env` file and run
```
docker run --name factorio-manager -d \
-v [yourpath]:/opt/factorio/saves \
-v [yourpath]:/opt/factorio/mods \
-v [yourpath]:/opt/factorio/config \
-v [yourpath]:/security \
-p 80:80 \
-p 443:443 \
-p 34197:34197/udp \
majormjr/factorio-server-manager
docker-compose -f docker-compose.simple.yaml up -d
```
### Factorio version
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.
## Accessing the application
Go to the port specified in your `docker run` command in your web browser. If running on localhost host access the application at https://localhost
Go to the domain specified in your `.env` file in your web browser. If running on localhost host 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
```
Creating factorio-server-manager ... done
```
you have to wait several seconds before FSM UI becomes available.
It may take some time for Let's Encrypt to issue the certificate, so for the first couple of minutes after starting the container you may see
"Your connection is not private" error when you open your Factorio Server Manager address in your browser. This error should disappear within
a couple of minutes, if configuration parameters are set correctly.
## Updating Credentials, adding and deleting users.
@ -48,25 +72,26 @@ 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.
This guide assumes that you mounted the volumes /security, /opt/factorio/saves, /opt/factorio/config and /opt/factorio/mods to your file system. Before doing anything we need to stop the old container using `docker stop factorio-manager`. To update Factorio you should then open the Dockerfile and change the Factorio version to the one desired. After that you need to rebuild the image using `docker build -t factorio-server-manager .`. Once completed you can simply rerun the command that you used to run the image in the first place. It's recommended to change the name to something including the version to keep track of the containers.
If you want to update Factorio to the latest version:
1. Save your game and stop Factorio server in FSM UI.
2. Run `docker-compose restart` (or `docker-compose -f docker-compose.simple.yaml restart` if you are using simple configuration).
Pull the latest container with `docker pull majormjr/factorio-server-manager` and start with the `docker run` command.
After container starts, latest Factorio version will be downloaded and installed.
## Security
A self generated SSL/TLS certificate is created when the container is first created and the webserver is accessible via HTTPS.
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.
### Changing SSL/TLS certificate
## 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`.
If you have your own SSL/TLS certificate then you can supply it to the Factorio Server Manager container.
### Creating release bundles
A Dockerfile-build file is included for creating the release bundles.
When first running the container you need to mount the security volume to your host machine by adding the security volume parameter `-v [yourpath]:/security`
The directory will contain a "server.key" file and a "server.crt" file.
If you replace these with a trusted SSL certificate and key, you should ensure that "server.crt" contains the whole certificate chain from the root of your CA.
To create the bundle build the Dockerfile-build file with the following command. The release bundles are output to the ./dist directory.
```
DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile-build -t ofsm-build --target=output -o dist .
```
## For everyone who actually read this thing to the end

9
docker/build-release.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
echo "Cloning ${FACTORIO_BRANCH}"
git clone -b ${FACTORIO_BRANCH} https://github.com/mroote/factorio-server-manager.git ${FACTORIO_ROOT}
echo "Creating build..."
make gen_release
echo "Copying build artifacts from ${PWD}"
mkdir -p /build
cp -v build/factorio-server-manager-linux.zip build/factorio-server-manager-windows.zip /build/

View File

@ -1,8 +1,10 @@
#!/bin/sh
#!/bin/bash
set -eou pipefail
(
cd ..
make build
cp build/factorio-server-manager-linux.zip docker/factorio-server-manager-linux.zip
)
docker build -f Dockerfile-local -t factorio-server-manager:dev .
echo "Cloning ${FAC_BRANCH}"
git clone -b ${FAC_BRANCH} https://github.com/mroote/factorio-server-manager.git ${FAC_ROOT}
echo "Creating build..."
make gen_release
echo "Copying build artifacts..."
cp -v build/* /build/
rm factorio-server-manager-linux.zip

View File

@ -0,0 +1,20 @@
version: "3"
services:
factorio-server-manager:
image: "ofsm/ofsm:develop"
container_name: "factorio-server-manager"
restart: "unless-stopped"
environment:
- "FACTORIO_VERSION=latest"
- "ADMIN_USER"
- "ADMIN_PASS"
- "RCON_PASS"
- "COOKIE_ENCRYPTION_KEY"
ports:
- "80:80"
- "34197:34197/udp"
volumes:
- "./fsm-data:/opt/fsm-data"
- "./factorio-data/saves:/opt/factorio/saves"
- "./factorio-data/mods:/opt/factorio/mods"
- "./factorio-data/config:/opt/factorio/config"

View File

@ -0,0 +1,65 @@
version: "3"
services:
factorio-server-manager:
image: "ofsm/ofsm:develop"
container_name: "factorio-server-manager"
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"
labels:
- "traefik.enable=true"
- "traefik.http.routers.fsm.entrypoints=websecure"
- "traefik.http.routers.fsm.rule=Host(`${DOMAIN_NAME}`)"
- "traefik.http.routers.fsm.tls=true"
- "traefik.http.routers.fsm.tls.certResolver=default"
- "traefik.http.routers.fsm.service=fsm"
#- "traefik.http.routers.fsm.middlewares=fsm-auth"
- "traefik.http.services.fsm.loadbalancer.server.port=80"
- "traefik.udp.routers.fsm.entrypoints=factorio"
- "traefik.udp.routers.fsm.service=fsm"
- "traefik.udp.services.fsm.loadbalancer.server.port=34197"
traefik:
image: "traefik:v2.2"
container_name: "traefik"
restart: "always"
command:
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.factorio.address=:34197/udp"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
- "--providers.docker"
- "--providers.docker.exposedByDefault=false"
- "--certificatesresolvers.default.acme.email=${EMAIL_ADDRESS}"
- "--certificatesresolvers.default.acme.storage=/etc/traefik/acme.json"
- "--certificatesresolvers.default.acme.tlschallenge=true"
ports:
- "80:80"
- "443:443"
- "34197:34197/udp"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "./traefik-data:/etc/traefik"
labels:
- "traefik.enable=true"
#- "traefik.http.middlewares.fsm-auth.basicauth.usersfile=/etc/traefik/.htpasswd"
#- "traefik.http.middlewares.fsm-auth.basicauth.realm=FSM"
#networks:
# default:
# external:
# name: "traefik"

View File

@ -1,17 +0,0 @@
version: '2'
services:
factorio-manager:
container_name: factorio-manager
image: "majormjr/factorio-server-manager"
restart: always
volumes:
- "/etc/localtime:/etc/localtime:ro"
- "/etc/timezone:/etc/timezone:ro"
- "[yourPath_optional]:/security"
- "[yourPath]:/opt/factorio/saves"
- "[yourPath]:/opt/factorio/mods"
- "[yourPath]:/opt/factorio/config"
ports:
- "80:80"
- "443:443"
- "34197:34197/udp"

51
docker/entrypoint.sh Executable file
View File

@ -0,0 +1,51 @@
#!/bin/sh
init_config() {
jq_cmd='.'
if [ -n $ADMIN_USER ]; then
jq_cmd="${jq_cmd} | .username = \"$ADMIN_USER\""
echo "Admin username is '$ADMIN_USER'"
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} | .log_file = \"/opt/fsm-data/factorio-server-manager.log\""
jq "${jq_cmd}" /opt/fsm/conf.json >/opt/fsm-data/conf.json
}
random_pass() {
LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | fold -w 24 | head -n 1
}
install_game() {
curl --location "https://www.factorio.com/get-download/${FACTORIO_VERSION}/headless/linux64" \
--output /tmp/factorio_${FACTORIO_VERSION}.tar.xz
tar -xf /tmp/factorio_${FACTORIO_VERSION}.tar.xz
rm /tmp/factorio_${FACTORIO_VERSION}.tar.xz
}
if [ ! -f /opt/fsm-data/conf.json ]; then
init_config
fi
install_game
cd /opt/fsm && ./factorio-server-manager --conf /opt/fsm-data/conf.json --dir /opt/factorio -port 80

View File

@ -1,18 +0,0 @@
#!/bin/sh
mkdir -p /security
if [ ! -f /security/server.key ]; then
echo "No SSL key found. generating new key and certificate"
openssl req \
-new \
-newkey rsa:2048 \
-days 365 \
-nodes\
-x509 \
-subj "/CN=localhost" \
-keyout /security/server.key \
-out /security/server.crt
fi
nohup nginx &
cd /opt/factorio-server-manager
./factorio-server-manager -dir '/opt/factorio'

View File

@ -1,74 +0,0 @@
user nginx;
worker_processes 1;
error_log logs/error.log warn;
pid run/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
client_max_body_size 100m;
#gzip on;
upstream goapp {
server 127.0.0.1:8080;
}
server {
listen 80 default_server;
return 301 https://$host$request_uri;
}
server {
listen 443 default_server;
ssl on;
ssl_certificate /security/server.crt;
ssl_certificate_key /security/server.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
ssl_prefer_server_ciphers on;
location /ws {
proxy_pass http://goapp;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_redirect off;
}
location /api {
proxy_pass http://goapp;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off;
}
location / {
root /opt/factorio-server-manager/app;
try_files $uri /index.html;
}
}
}

8672
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -10,36 +10,34 @@
"watch-poll": "npm run watch -- --watch-poll",
"build": "npm run production",
"prod": "npm run production",
"production": "webpack --config=webpack.config.js --mode=production --hide-modules --progress",
"production": "cross-env NODE_ENV=production webpack --config=webpack.config.js --mode=production --hide-modules --progress",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mroote/factorio-server-manager.git"
"url": "git+https://github.com/OpenFactorioServerManager/factorio-server-manager.git"
},
"author": "Mitch Roote <mitch@r00t.ca>",
"license": "MIT",
"bugs": {
"url": "https://github.com/mroote/factorio-server-manager/issues"
"url": "https://github.com/OpenFactorioServerManager/factorio-server-manager/issues"
},
"homepage": "https://github.com/mroote/factorio-server-manager#readme",
"homepage": "https://github.com/OpenFactorioServerManager/factorio-server-manager#readme",
"devDependencies": {
"@babel/core": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"@babel/preset-react": "^7.0.0",
"@fortawesome/fontawesome-free": "^5.8.2",
"admin-lte": "^3.0.0-alpha.2",
"@babel/core": "^7.11.6",
"@babel/preset-env": "^7.11.5",
"@babel/preset-react": "^7.10.4",
"autoprefixer": "^9.8.6",
"babel-loader": "^8.0.6",
"bootstrap": "^4.3.1",
"bootstrap-fileinput": "^5.0.3",
"classnames": "^2.2.6",
"cross-env": "^7.0.2",
"css-loader": "^2.1.0",
"file-loader": "^3.0.1",
"jquery": "^3.4.1",
"locks": "^0.2.2",
"mini-css-extract-plugin": "^0.7.0",
"node-sass": "^4.12.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss-loader": "^3.0.0",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
@ -48,11 +46,20 @@
"react-router-dom": "^5.0.0",
"resolve-url-loader": "^3.1.0",
"sass-loader": "^7.1.0",
"semver": "^6.1.1",
"sweetalert2": "^8.11.6",
"sweetalert2-react-content": "^1.1.0",
"webpack": "^4.32.2",
"webpack-cli": "^3.3.2",
"style-loader": "^1.3.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"webpack-fix-style-only-entries": "^0.2.0"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "^0.1.11",
"axios": "^0.19.2",
"fuse.js": "^6.4.1",
"react-hook-form": "^5.7.2",
"regenerator-runtime": "^0.13.7",
"semver": "^6.1.1",
"tailwindcss": "^1.8.13"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

5
src/.env.example Normal file
View File

@ -0,0 +1,5 @@
factorio_username=
factorio_password=
conf=../../conf.json.example
mod_dir=dev
mod_pack_dir=dev_pack

View File

@ -1,8 +1,10 @@
package main
package api
import (
"github.com/mroote/factorio-server-manager/bootstrap"
"log"
"os"
"sync"
"github.com/apexskier/httpauth"
)
@ -19,8 +21,18 @@ type User struct {
Email string `json:"email"`
}
func initAuth() *AuthHTTP {
return &AuthHTTP{}
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
}
func (auth *AuthHTTP) CreateAuth(backendFile string, cookieKey string) error {

659
src/api/handlers.go Normal file
View File

@ -0,0 +1,659 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"github.com/mroote/factorio-server-manager/bootstrap"
"github.com/mroote/factorio-server-manager/factorio"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/gorilla/mux"
)
const readHttpBodyError = "Could not read the Request Body."
type JSONResponseFileInput struct {
Success bool `json:"success"`
Data interface{} `json:"data,string"`
Error string `json:"error"`
ErrorKeys []int `json:"errorkeys"`
}
func WriteResponse(w http.ResponseWriter, data interface{}) {
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("Error writing response: %s", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func ReadRequestBody(w http.ResponseWriter, r *http.Request, resp *interface{}) (body []byte, err error) {
if r.Body == nil {
*resp = fmt.Sprintf("%s: no request body", readHttpBodyError)
log.Println(*resp)
w.WriteHeader(http.StatusBadRequest)
return nil, errors.New("no request body")
}
body, err = ioutil.ReadAll(r.Body)
if err != nil {
*resp = fmt.Sprintf("%s: %s", readHttpBodyError, err)
log.Println(*resp)
w.WriteHeader(http.StatusInternalServerError)
}
return
}
// Lists all save files in the factorio/saves directory
func ListSaves(w http.ResponseWriter, r *http.Request) {
var resp interface{}
config := bootstrap.GetConfig()
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
savesList, err := factorio.ListSaves(config.FactorioSavesDir)
if err != nil {
resp = fmt.Sprintf("Error listing save files: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
loadLatest := factorio.Save{Name: "Load Latest"}
savesList = append(savesList, loadLatest)
resp = savesList
}
func DLSave(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/octet-stream")
config := bootstrap.GetConfig()
vars := mux.Vars(r)
save := vars["save"]
saveName := filepath.Join(config.FactorioSavesDir, save)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", save))
log.Printf("%s downloading: %s", r.Host, saveName)
http.ServeFile(w, r, saveName)
}
func UploadSave(w http.ResponseWriter, r *http.Request) {
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
log.Println("Uploading save file")
r.ParseMultipartForm(32 << 20)
config := bootstrap.GetConfig()
for _, saveFile := range r.MultipartForm.File["savefile"] {
ext := filepath.Ext(saveFile.Filename)
if ext != "zip" {
// Only zip-files allowed
resp = fmt.Sprintf("Fileformat {%s} is not allowed", ext)
w.WriteHeader(http.StatusUnsupportedMediaType)
}
file, err := saveFile.Open()
if err != nil {
resp = fmt.Sprintf("Error opening uploaded saveFile: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer file.Close()
out, err := os.Create(filepath.Join(config.FactorioSavesDir, saveFile.Filename))
if err != nil {
resp = fmt.Sprintf("Error creating new savefile to copy uploaded on to: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
resp = fmt.Sprintf("Error coping uploaded file to created file on disk: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
resp = "Uploading files successful"
}
// Deletes provided save
func RemoveSave(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
vars := mux.Vars(r)
name := vars["save"]
save, err := factorio.FindSave(name)
if err != nil {
resp = fmt.Sprintf("Error finding save {%s}: %s", name, err)
log.Println(resp)
w.WriteHeader(http.StatusBadRequest)
return
}
err = save.Remove()
if err != nil {
resp = fmt.Sprintf("Error removing save {%s}: %s", name, err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
// save was removed
resp = fmt.Sprintf("Removed save: %s", save.Name)
}
// Launches Factorio server binary with --create flag to create save
// Url must include save name for creation of savefile
func CreateSaveHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
vars := mux.Vars(r)
saveName := vars["save"]
if saveName == "" {
resp = fmt.Sprintf("Error creating save, no save name provided: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusBadRequest)
return
}
config := bootstrap.GetConfig()
saveFile := filepath.Join(config.FactorioSavesDir, saveName)
cmdOut, err := factorio.CreateSave(saveFile)
if err != nil {
resp = fmt.Sprintf("Error creating save {%s}: %s", saveName, err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = fmt.Sprintf("Save %s created successfully. Command output: \n%s", saveName, cmdOut)
}
// LogTail returns last lines of the factorio-current.log file
func LogTail(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
config := bootstrap.GetConfig()
resp, err = factorio.TailLog()
if err != nil {
resp = fmt.Sprintf("Could not tail %s: %s", config.FactorioLog, err)
return
}
}
// LoadConfig returns JSON response of config.ini file
func LoadConfig(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
config := bootstrap.GetConfig()
configContents, err := factorio.LoadConfig(config.FactorioConfigFile)
if err != nil {
resp = fmt.Sprintf("Could not retrieve config.ini: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = configContents
log.Printf("Sent config.ini response")
}
func StartServer(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
var server = factorio.GetFactorioServer()
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
if server.GetRunning() {
resp = "Factorio server is already running"
w.WriteHeader(http.StatusConflict)
return
}
log.Printf("Starting Factorio server.")
body, err := ReadRequestBody(w, r, &resp)
if err != nil {
return
}
log.Printf("Starting Factorio server with settings: %v", string(body))
err = json.Unmarshal(body, &server)
if err != nil {
resp = fmt.Sprintf("Error unmarshalling server settings JSON: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusBadRequest)
return
}
// Check if savefile was submitted with request to start server.
if server.Savefile == "" {
resp = "Error starting Factorio server: No save file provided"
log.Println(resp)
w.WriteHeader(http.StatusBadRequest)
return
}
go func() {
err = server.Run()
if err != nil {
log.Printf("Error starting Factorio server: %+v", err)
return
}
}()
timeout := 0
for timeout <= 3 {
time.Sleep(1 * time.Second)
if server.GetRunning() {
log.Printf("Running Factorio server detected")
break
} else {
log.Printf("Did not detect running Factorio server attempt: %+v", timeout)
}
timeout++
}
if server.GetRunning() == false {
resp = fmt.Sprintf("Error starting Factorio server: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = fmt.Sprintf("Factorio server with save: %s started on port: %d", server.Savefile, server.Port)
log.Println(resp)
}
func StopServer(w http.ResponseWriter, r *http.Request) {
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var server = factorio.GetFactorioServer()
if server.GetRunning() {
err := server.Stop()
if err != nil {
resp = fmt.Sprintf("Error stopping factorio server: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = fmt.Sprintf("Factorio server stopped")
log.Println(resp)
} else {
resp = "Factorio server is not running"
w.WriteHeader(http.StatusConflict)
return
}
}
func KillServer(w http.ResponseWriter, r *http.Request) {
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var server = factorio.GetFactorioServer()
if server.GetRunning() {
err := server.Kill()
if err != nil {
resp = fmt.Sprintf("Error killing factorio server: %s", err)
log.Println(resp)
return
}
log.Printf("Killed Factorio server.")
resp = fmt.Sprintf("Factorio server killed")
} else {
resp = "Factorio server is not running"
w.WriteHeader(http.StatusBadRequest)
}
}
func CheckServer(w http.ResponseWriter, r *http.Request) {
resp := map[string]string{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var server = factorio.GetFactorioServer()
if server.GetRunning() {
resp["status"] = "running"
resp["port"] = strconv.Itoa(server.Port)
resp["savefile"] = server.Savefile
resp["address"] = server.BindIP
} else {
resp["status"] = "stopped"
}
}
func FactorioVersion(w http.ResponseWriter, r *http.Request) {
resp := map[string]string{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var server = factorio.GetFactorioServer()
resp["version"] = server.Version.String()
resp["base_mod_version"] = server.BaseModVersion
}
// 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) {
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
}
func LoginUser(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
// add resp to the response
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
body, err := ReadRequestBody(w, r, &resp)
if err != nil {
return
}
user, err := UnmarshallUserJson(body, &resp, 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, "/")
if err != nil {
resp = fmt.Sprintf("Error loggin in user: %s, error: %s", user.Username, err)
log.Println(resp)
return
}
log.Printf("User: %s, logged in successfully", user.Username)
}
func LogoutUser(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
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)
return
}
resp = "User logged out successfully."
}
func GetCurrentLogin(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
// add resp to the response
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
Auth := GetAuth()
user, err := Auth.aaa.CurrentUser(w, r)
if err != nil {
resp = fmt.Sprintf("Error getting user status: %s, error: %s", user.Username, err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = user
}
func ListUsers(w http.ResponseWriter, r *http.Request) {
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
Auth := GetAuth()
users, err := Auth.listUsers()
if err != nil {
resp = fmt.Sprintf("Error listing users: %s", err)
log.Println(resp)
return
}
resp = users
}
func AddUser(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, err := ReadRequestBody(w, r, &resp)
if err != nil {
return
}
log.Printf("Adding user: %v", string(body))
user, err := UnmarshallUserJson(body, &resp, w)
if err != nil {
return
}
Auth := GetAuth()
err = Auth.addUser(user.Username, user.Password, user.Email, user.Role)
if err != nil {
resp = fmt.Sprintf("Error in adding user {%s}: %s", user.Username, err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = fmt.Sprintf("User: %s successfully added.", user.Username)
}
func RemoveUser(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, err := ReadRequestBody(w, r, &resp)
if err != nil {
return
}
user, err := UnmarshallUserJson(body, &resp, w)
if err != nil {
return
}
Auth := GetAuth()
err = Auth.removeUser(user.Username)
if err != nil {
resp = fmt.Sprintf("Error in removing user {%s}, error: %s", user.Username, err)
log.Println(resp)
return
}
resp = fmt.Sprintf("User: %s successfully removed.", user.Username)
}
// GetServerSettings returns JSON response of server-settings.json file
func GetServerSettings(w http.ResponseWriter, r *http.Request) {
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var server = factorio.GetFactorioServer()
resp = server.Settings
log.Printf("Sent server settings response")
}
func UpdateServerSettings(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, err := ReadRequestBody(w, r, &resp)
if err != nil {
return
}
log.Printf("Received settings JSON: %s", body)
var server = factorio.GetFactorioServer()
// Race Condition while unmarshal possible
var wg sync.WaitGroup
wg.Add(1)
go func() {
err = json.Unmarshal(body, &server.Settings)
wg.Done()
}()
// Wait for unmarshal to avoid race condition
wg.Wait()
if err != nil {
resp = fmt.Sprintf("Error unmarhaling server settings JSON: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusBadRequest)
return
}
settings, err := json.MarshalIndent(&server.Settings, "", " ")
if err != nil {
resp = fmt.Sprintf("Failed to marshal server settings: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
config := bootstrap.GetConfig()
err = ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.SettingsFile), settings, 0644)
if err != nil {
resp = fmt.Sprintf("Failed to save server settings: %v\n", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Printf("Saved Factorio server settings in server-settings.json")
if (server.Version.Greater(factorio.Version{0, 17, 0})) {
// save admins to adminJson
admins, err := json.MarshalIndent(server.Settings["admins"], "", " ")
if err != nil {
resp = fmt.Sprintf("Failed to marshal admins-Setting: %s", err)
log.Println(resp)
return
}
err = ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile), admins, 0664)
if err != nil {
resp = fmt.Sprintf("Failed to save admins: %s", err)
log.Println(resp)
return
}
}
resp = fmt.Sprintf("Settings successfully saved")
}

View File

@ -0,0 +1,522 @@
package api
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"
)
func CheckModPackExists(modPackMap factorio.ModPackMap, modPackName string, w http.ResponseWriter, resp interface{}) 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")
}
return nil
}
func CreateNewModPackMap(w http.ResponseWriter, resp *interface{}) (modPackMap factorio.ModPackMap, 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)
}
return
}
func ReadModPackRequest(w http.ResponseWriter, r *http.Request, resp *interface{}) (err error, packMap factorio.ModPackMap, modPackName string) {
vars := mux.Vars(r)
modPackName = vars["modpack"]
packMap, err = CreateNewModPackMap(w, resp)
if err != nil {
return
}
if err = CheckModPackExists(packMap, modPackName, w, resp); err != nil {
return
}
return
}
//////////////////////
// Mod Pack Handler //
//////////////////////
func ModPackListHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
modPackMap, err := CreateNewModPackMap(w, &resp)
if err != nil {
return
}
resp = modPackMap.ListInstalledModPacks()
}
func ModPackCreateHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var modPackStruct struct {
Name string `json:"name"`
}
err = ReadFromRequestBody(w, r, &resp, &modPackStruct)
if err != nil {
return
}
modPackMap, err := CreateNewModPackMap(w, &resp)
if err != nil {
return
}
err = modPackMap.CreateModPack(modPackStruct.Name)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp = fmt.Sprintf("Error creating modpack file: %s", err)
log.Println(resp)
return
}
resp = modPackMap.ListInstalledModPacks()
}
func ModPackDeleteHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp)
if err != nil {
return
}
err = modPackMap.DeleteModPack(modPackName)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp = fmt.Sprintf("Error deleting modpack file: %s", err)
log.Println(resp)
return
}
resp = modPackName
}
func ModPackDownloadHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
err, _, modPackName := ReadModPackRequest(w, r, &resp)
if err != nil {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
WriteResponse(w, resp)
return
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", modPackName))
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
config := bootstrap.GetConfig()
//iterate over folder and create everything in the zip
err = filepath.Walk(filepath.Join(config.FactorioModPackDir, modPackName), func(path string, info os.FileInfo, err error) error {
if info.IsDir() == false {
writer, err := zipWriter.Create(info.Name())
if err != nil {
log.Printf("error on creating new file inside zip: %s", err)
return err
}
file, err := os.Open(path)
if err != nil {
log.Printf("error on opening modfile: %s", err)
return err
}
// Close file, when function returns
defer func() {
err2 := file.Close()
if err == nil && err2 != nil {
log.Printf("Error closing file: %s", err2)
err = err2
}
}()
_, err = io.Copy(writer, file)
if err != nil {
log.Printf("error on copying file into zip: %s", err)
return err
}
}
return nil
})
if err != nil {
resp = fmt.Sprintf("error on walking over the modpack: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
WriteResponse(w, resp)
return
}
w.Header().Set("Content-Type", "application/zip;charset=UTF-8")
}
func ModPackLoadHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp)
if err != nil {
return
}
err = modPackMap[modPackName].LoadModPack()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp = fmt.Sprintf("Error loading modpack file: %s", err)
log.Println(resp)
return
}
resp = modPackMap[modPackName].Mods.ListInstalledMods()
}
//////////////////////////////////
// Mods inside Mod Pack Handler //
//////////////////////////////////
func ModPackModListHandler(w http.ResponseWriter, r *http.Request) {
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp)
if err != nil {
return
}
resp = modPackMap[modPackName].Mods.ListInstalledMods()
}
func ModPackModToggleHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
err, packMap, packName := ReadModPackRequest(w, r, &resp)
if err != nil {
return
}
var modPackStruct struct {
ModName string `json:"name"`
}
ReadFromRequestBody(w, r, &resp, &modPackStruct)
if err != nil {
return
}
err, resp = packMap[packName].Mods.ModSimpleList.ToggleMod(modPackStruct.ModName)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp = fmt.Sprintf("Error toggling mod inside modPack: %s", err)
log.Println(resp)
return
}
}
func ModPackModDeleteHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
err, packMap, packName := ReadModPackRequest(w, r, &resp)
if err != nil {
return
}
var modPackStruct struct {
Name string `json:"name"`
}
err = ReadFromRequestBody(w, r, &resp, &modPackStruct)
if err != nil {
return
}
err = packMap[packName].Mods.DeleteMod(modPackStruct.Name)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp = fmt.Sprintf("Error deleting mod {%s} in modpack {%s}: %s", modPackStruct.Name, packName, err)
log.Println(resp)
return
}
resp = true
}
func ModPackModUpdateHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//Get Data out of the request
var modPackStruct struct {
ModName string `json:"modName"`
DownloadUrl string `json:"downloadUrl"`
Filename string `json:"filename"`
}
err = ReadFromRequestBody(w, r, &resp, &modPackStruct)
if err != nil {
return
}
err, packMap, packName := ReadModPackRequest(w, r, &resp)
if err != nil {
return
}
err = packMap[packName].Mods.UpdateMod(modPackStruct.ModName, modPackStruct.DownloadUrl, modPackStruct.Filename)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp = fmt.Sprintf("Error updating mod {%s} in modpack {%s}: %s", modPackStruct.ModName, packName, err)
log.Println(resp)
return
}
installedMods := packMap[packName].Mods.ListInstalledMods().ModsResult
var found = false
for _, mod := range installedMods {
if mod.Name == modPackStruct.ModName {
resp = mod
found = true
return
}
}
if !found {
resp = fmt.Sprintf(`Could not find mod %s`, modPackStruct.ModName)
log.Println(resp)
w.WriteHeader(http.StatusNotFound)
return
}
}
func ModPackModDeleteAllHandler(w http.ResponseWriter, r *http.Request) {
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
err, packMap, packName := ReadModPackRequest(w, r, &resp)
if err != nil {
return
}
// Delete Modpack
err = packMap.DeleteModPack(packName)
if err != nil {
resp = fmt.Sprintf("Error deleting modPackDir: %s", err)
log.Println(resp)
return
}
// recreate modPack without mods
err = packMap.CreateEmptyModPack(packName)
if err != nil {
resp = fmt.Sprintf("Error recreating modPackDir: %s", err)
log.Println(resp)
return
}
resp = true
}
func ModPackModUploadHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
formFile, fileHeader, err := r.FormFile("mod_file")
if err != nil {
resp = fmt.Sprintf("error getting uploaded file: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusBadRequest)
return
}
defer formFile.Close()
err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp)
err = modPackMap[modPackName].Mods.UploadMod(formFile, fileHeader)
if err != nil {
resp = fmt.Sprintf("error saving file to modPack: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = modPackMap[modPackName].Mods.ListInstalledMods()
}
func ModPackModPortalInstallHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
// Get Data out of the request
var data struct {
DownloadURL string `json:"downloadUrl"`
Filename string `json:"fileName"`
ModName string `json:"modName"`
}
err = ReadFromRequestBody(w, r, &resp, &data)
if err != nil {
return
}
err, packMap, packName := ReadModPackRequest(w, r, &resp)
if err != nil {
return
}
modList := packMap[packName].Mods
err = modList.DownloadMod(data.DownloadURL, data.Filename, data.ModName)
if err != nil {
resp = fmt.Sprintf("Error downloading a mod: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = modList.ListInstalledMods()
}
func ModPackModPortalInstallMultipleHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var data []struct {
Name string `json:"name"`
Version factorio.Version `json:"version"`
}
err = ReadFromRequestBody(w, r, &resp, &data)
if err != nil {
return
}
err, packMap, packName := ReadModPackRequest(w, r, &resp)
if err != nil {
return
}
modList := packMap[packName].Mods
for _, datum := range data {
details, err, statusCode := factorio.ModPortalModDetails(datum.Name)
if err != nil || statusCode != http.StatusOK {
resp = fmt.Sprintf("Error in getting mod details from mod portal: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
//find correct mod-version
var found = false
for _, release := range details.Releases {
if release.Version.Equals(datum.Version) {
found = true
err := modList.DownloadMod(release.DownloadURL, release.FileName, details.Name)
if err != nil {
resp = fmt.Sprintf("Error downloading mod {%s}, error: %s", details.Name, err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
break
}
}
if !found {
log.Printf("Error downloading mod {%s}, error: %s", details.Name, "version not found")
w.WriteHeader(http.StatusInternalServerError)
}
}
resp = modList.ListInstalledMods()
}

View File

@ -0,0 +1,660 @@
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"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func SetupModPacks(t *testing.T, empty bool, emptyMods bool) {
var err error
config := bootstrap.GetConfig()
// check if dev directory exists and create it
if _, err = os.Stat(config.FactorioModPackDir); os.IsNotExist(err) {
err = os.Mkdir(config.FactorioModPackDir, 0775)
}
assert.NoError(t, err, `Error creating %s directory`, config.FactorioModPackDir)
if !empty {
// check if dev directory exists and create it
if _, err = os.Stat(config.FactorioModPackDir + "/test"); os.IsNotExist(err) {
err = os.Mkdir(config.FactorioModPackDir+"/test", 0775)
}
assert.NoError(t, err, `Error creating "%s/test" directory`, config.FactorioModPackDir)
modList, err := factorio.NewMods(config.FactorioModPackDir + "/test")
assert.NoError(t, err, "error creating mods")
if !emptyMods {
err = modList.DownloadMod("/download/belt-balancer/5e9f9db4bf9d30000c5303f2", "belt-balancer_2.1.3.zip", "belt-balancer")
assert.NoError(t, err, `Error downloading Mod "belt-balancer"`)
err = modList.DownloadMod("/download/train-station-overview/5e8a0a8ee8864f000d0cb022", "train-station-overview_2.0.3.zip", "train-station-overview")
assert.NoError(t, err, `Error downloading Mod "train-station-overview"`)
}
}
}
func CleanupModPacks(t *testing.T) {
config := bootstrap.GetConfig()
err := os.RemoveAll(config.FactorioModPackDir)
assert.NoError(t, err, `Error removing directory %s`, config.FactorioModPackDir)
}
func UnknownModpackTest(t *testing.T, method string, baseRoute string, route string, handlerFunc http.HandlerFunc) {
t.Run("unknown modpack", func(t *testing.T) {
SetupModPacks(t, true, true)
defer CleanupModPacks(t)
requestBody := strings.NewReader(`{"name": "belt-balancer"}`)
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusNotFound, "")
})
}
func ModPackUnknownModTest(t *testing.T, method string, baseRoute string, route string, handlerFunc http.HandlerFunc) {
t.Run("unknown mod", func(t *testing.T) {
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
requestBody := strings.NewReader(`{"name": "askhdbali"}`)
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
})
}
func ModPackEmptyBodyTest(t *testing.T, method string, baseRoute string, route string, handlerFunc http.HandlerFunc) {
t.Run("empty body", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
SetupMods(t, false)
defer CleanupMods(t)
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusBadRequest, "")
})
}
func ModPackInvalidJsonBodyTest(t *testing.T, method string, baseRoute string, route string, handlerFunc http.HandlerFunc) {
t.Run("invalid json body", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
SetupMods(t, true)
defer CleanupMods(t)
requestBody := strings.NewReader(`{`)
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusBadRequest, "")
})
}
func TestModPackListHandler(t *testing.T) {
CheckShort(t)
method := "GET"
route := "/mods/packs/list"
handlerFunc := ModPackListHandler
t.Run("success", func(t *testing.T) {
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
expected := `[{"name":"test","mods":{"mods":[{"name":"belt-balancer","version":"2.1.3","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.3","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}}]`
CallRoute(t, method, route, route, nil, handlerFunc, http.StatusOK, expected)
})
t.Run("empty modpack", func(t *testing.T) {
SetupModPacks(t, true, false)
defer CleanupModPacks(t)
expected := `[]`
CallRoute(t, method, route, route, nil, handlerFunc, http.StatusOK, expected)
})
}
func TestModPackCreateHandler(t *testing.T) {
CheckShort(t)
method := "POST"
route := "/mods/packs/create"
handlerFunc := ModPackCreateHandler
t.Run("success", func(t *testing.T) {
SetupModPacks(t, true, false)
defer CleanupModPacks(t)
SetupMods(t, false)
defer CleanupMods(t)
requestBody := strings.NewReader(`{"name": "test"}`)
expected := `[{"name":"test","mods":{"mods":[{"name":"belt-balancer","version":"2.1.3","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.3","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}}]`
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, expected)
})
ModPackEmptyBodyTest(t, method, route, route, handlerFunc)
ModPackInvalidJsonBodyTest(t, method, route, route, handlerFunc)
}
func TestModPackDeleteHandler(t *testing.T) {
CheckShort(t)
method := "POST"
baseRoute := "/mods/packs/{modpack}/delete"
route := "/mods/packs/test/delete"
handlerFunc := ModPackDeleteHandler
t.Run("success", func(t *testing.T) {
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusOK, `"test"`)
})
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
}
func TestModPackLoadHandler(t *testing.T) {
CheckShort(t)
method := "POST"
baseRoute := "/mods/packs/{modpack}/load"
route := "/mods/packs/test/load"
handlerFunc := ModPackLoadHandler
t.Run("load mods", func(t *testing.T) {
config := bootstrap.GetConfig()
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
SetupMods(t, true)
defer CleanupMods(t)
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.3","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.3","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusOK, expected)
// check if mods are really loaded
packMap, err := factorio.NewModPackMap()
assert.NoError(t, err, "Error creating modPackMap")
modList, err := factorio.NewMods(config.FactorioModsDir)
assert.NoError(t, err, "Error creating mods object")
packModsJson, err := json.Marshal(packMap["test"].Mods)
assert.NoError(t, err, "Error marshalling mods from modPack")
modsJson, err := json.Marshal(modList)
assert.NoError(t, err, "Error marshalling mods object")
assert.JSONEq(t, string(packModsJson), string(modsJson), "loaded mods and modPack are not identical")
})
t.Run("load empty modpack", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
SetupMods(t, false)
defer CleanupMods(t)
expected := `{"mods":[]}`
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusOK, expected)
// check if mods are really loaded
packMap, err := factorio.NewModPackMap()
assert.NoError(t, err, "Error creating modPackMap")
config := bootstrap.GetConfig()
modList, err := factorio.NewMods(config.FactorioModsDir)
assert.NoError(t, err, "Error creating mods object")
packModsJson, err := json.Marshal(packMap["test"].Mods)
assert.NoError(t, err, "Error marshalling mods from modPack")
modsJson, err := json.Marshal(modList)
assert.NoError(t, err, "Error marshalling mods object")
assert.JSONEq(t, string(packModsJson), string(modsJson), "loaded mods and modPack are not identical")
})
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
}
func TestModPackModListHandler(t *testing.T) {
CheckShort(t)
method := "GET"
baseRoute := "/mods/packs/{modpack}/list"
route := "/mods/packs/test/list"
handlerFunc := ModPackModListHandler
t.Run("success", func(t *testing.T) {
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.3","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.3","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusOK, expected)
})
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
}
func TestModPackModToggleHandler(t *testing.T) {
CheckShort(t)
method := "POST"
baseRoute := "/mods/packs/{modpack}/mod/toggle"
route := "/mods/packs/test/mod/toggle"
handlerFunc := ModPackModToggleHandler
t.Run("success", func(t *testing.T) {
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
requestBody := strings.NewReader(`{"name": "belt-balancer"}`)
// mod is now deactivated
expected := "false"
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusOK, expected)
// check if changes happened
packMap, err := factorio.NewModPackMap()
assert.NoError(t, err, "Error creating modPackMap")
found := false
for _, mod := range packMap["test"].Mods.ModSimpleList.Mods {
if mod.Name == "belt-balancer" {
// this mod has to be deactivated now
if mod.Enabled {
t.Fatalf("Mod is wrongly enabled, it should be disabled by now")
}
found = true
break
}
}
if !found {
t.Fatalf("Mod not found")
}
// toggle again, to check if the other direction also works
// mod is now activated again
expected = "true"
// reset request body, it has to be red again
requestBody.Seek(0, 0)
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusOK, expected)
packMap, err = factorio.NewModPackMap()
assert.NoError(t, err, "Error creating modPackMap")
found = false
for _, mod := range packMap["test"].Mods.ModSimpleList.Mods {
if mod.Name == "belt-balancer" {
// this mod has to be deactivated now
if !mod.Enabled {
t.Fatalf("Mod is wrongly disabled, it should be enabled again")
}
found = true
break
}
}
if !found {
t.Fatalf("Mod not found")
}
})
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
ModPackUnknownModTest(t, method, baseRoute, route, handlerFunc)
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
ModPackInvalidJsonBodyTest(t, method, baseRoute, route, handlerFunc)
}
func TestModPackModDeleteHandler(t *testing.T) {
CheckShort(t)
method := "POST"
baseRoute := "/mods/packs/{modpack}/mod/delete"
route := "/mods/packs/test/mod/delete"
handlerFunc := ModPackModDeleteHandler
t.Run("success", func(t *testing.T) {
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
requestBody := strings.NewReader(`{"name": "belt-balancer"}`)
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusOK, `true`)
// check if mod is really not installed anymore
packMap, err := factorio.NewModPackMap()
assert.NoError(t, err, "Error creating modPackMap")
if packMap["test"].Mods.ModSimpleList.CheckModExists("belt-balancer") {
t.Fatalf("Mod is still installed, it should be gone by now")
}
})
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
ModPackUnknownModTest(t, method, baseRoute, route, handlerFunc)
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
ModPackInvalidJsonBodyTest(t, method, baseRoute, route, handlerFunc)
}
func TestModPackModDeleteAllHandler(t *testing.T) {
CheckShort(t)
method := "POST"
baseRoute := "/mods/packs/{modpack}/mod/delete/all"
route := "/mods/packs/test/mod/delete/all"
handlerFunc := ModPackModDeleteAllHandler
t.Run("success", func(t *testing.T) {
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusOK, "true")
// check if really empty
packMap, err := factorio.NewModPackMap()
assert.NoError(t, err, "Error creating modPackMap")
if len(packMap["test"].Mods.ModInfoList.Mods) != 0 {
t.Fatal("There are still mods in the modpack")
}
})
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
}
func TestModPackModUpdateHandler(t *testing.T) {
CheckShort(t)
method := "POST"
baseRoute := "/mods/packs/{modpack}/mod/update"
route := "/mods/packs/test/mod/update"
handlerFunc := ModPackModUpdateHandler
requestBodySuccess := `{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`
t.Run("success", func(t *testing.T) {
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
expected := `{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}`
CallRoute(t, method, baseRoute, route, strings.NewReader(requestBodySuccess), handlerFunc, http.StatusOK, expected)
})
t.Run("success with disabled mod", func(t *testing.T) {
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
// disable "belt-balancer" mod, so we can test, if it is still deactivated after
packMap, err := factorio.NewModPackMap()
assert.NoError(t, err, "Error creating modPackMap")
err, _ = packMap["test"].Mods.ModSimpleList.ToggleMod("belt-balancer")
assert.NoError(t, err, "Error toggling mod")
expected := `{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":false}`
CallRoute(t, method, baseRoute, route, strings.NewReader(requestBodySuccess), handlerFunc, http.StatusOK, expected)
})
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
ModPackInvalidJsonBodyTest(t, method, baseRoute, route, handlerFunc)
t.Run("unknown mod", func(t *testing.T) {
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
requestBody := `{"modName": "asldbsac", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`
CallRoute(t, method, baseRoute, route, strings.NewReader(requestBody), handlerFunc, http.StatusNotFound, "")
})
t.Run("wrong download link", func(t *testing.T) {
SetupModPacks(t, false, false)
defer CleanupModPacks(t)
requestBody := `{"modName": "asldbsac", "downloadUrl": "/download/belt-balancer/95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`
CallRoute(t, method, baseRoute, route, strings.NewReader(requestBody), handlerFunc, http.StatusInternalServerError, "")
// check if old mod is still there
packMap, err := factorio.NewModPackMap()
assert.NoError(t, err, "Error creating modPackMap")
var found = false
for _, mod := range packMap["test"].Mods.ModInfoList.Mods {
if mod.Name == "belt-balancer" {
found = true
}
}
if !found {
t.Fatal(`Mod "belt-balancer" is not there anymore`)
}
})
}
func ModPackModUploadRequest(t *testing.T, body bool, filePath string) *httptest.ResponseRecorder {
method := "POST"
baseRoute := "/mods/packs/{modpack}/mod/upload"
route := "/mods/packs/test/mod/upload"
handlerFunc := ModPackModUploadHandler
var err error
requestBody := &bytes.Buffer{}
writer := multipart.NewWriter(requestBody)
if body {
file, err := os.Open(filePath)
if err == nil {
assert.NoError(t, err, "error opening mod file")
formFile, err := writer.CreateFormFile("mod_file", filepath.Base(filePath))
assert.NoError(t, err, "error creating formFileWriter")
_, err = io.Copy(formFile, file)
assert.NoError(t, err, "error copying file to form")
}
}
err = writer.Close()
if err != nil {
t.Fatalf("error closing the multipart writer: %s", err)
}
// create request to send
request, err := http.NewRequest(method, route, requestBody)
assert.NoError(t, err, "Error creating request")
request.Header.Set("Content-Type", writer.FormDataContentType())
// create response recorder
recorder := httptest.NewRecorder()
// get the handler, where the request is handled
router := mux.NewRouter()
router.HandleFunc(baseRoute, handlerFunc)
// call the handler directly
router.ServeHTTP(recorder, request)
return recorder
}
func TestModPackModUploadHandler(t *testing.T) {
CheckShort(t)
method := "POST"
baseRoute := "/mods/packs/{modpack}/mod/upload"
route := "/mods/packs/test/mod/upload"
handlerFunc := ModPackModUploadHandler
t.Run("success", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
recorder := ModPackModUploadRequest(t, true, "../factorio_testfiles/belt-balancer_2.1.3.zip")
// status has to be 200
if recorder.Code != http.StatusOK {
t.Fatalf("Wrong Status Code. expected %v - got %v", http.StatusOK, recorder.Code)
}
// check if mod is uploaded correctly
packMap, err := factorio.NewModPackMap()
assert.NoError(t, err, "error creating modPackMap")
expected := factorio.ModsResultList{
ModsResult: []factorio.ModsResult{
{
ModInfo: factorio.ModInfo{
Name: "belt-balancer",
Version: "2.1.3",
Title: "Belt Balancer",
Author: "knoxfighter",
FileName: "belt-balancer_2.1.3.zip",
FactorioVersion: factorio.Version{0, 18, 0, 0},
Dependencies: nil,
Compatibility: true,
},
Enabled: true,
},
},
}
actual := packMap["test"].Mods.ListInstalledMods()
assert.Equal(t, expected, actual, `New mod is not correctly installed. expected "%v" - actual "%v"`, expected, actual)
})
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
t.Run("empty file", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
recorder := ModPackModUploadRequest(t, true, "")
assert.Equal(t, http.StatusBadRequest, recorder.Code, "wrong response code.")
})
t.Run("invalid mod file (txt-file)", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
recorder := ModPackModUploadRequest(t, false, "../factorio_testfiles/file_usage.txt")
assert.Equal(t, http.StatusBadRequest, recorder.Code, "wrong response code.")
})
t.Run("invalid mod file (zip-file)", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
recorder := ModPackModUploadRequest(t, true, "../factorio_testfiles/invalid_mod.zip")
assert.Equal(t, http.StatusInternalServerError, recorder.Code, "wrong response code.")
})
}
func TestModPackModPortalInstallHandler(t *testing.T) {
CheckShort(t)
method := "POST"
baseRoute := "/mods/packs/{modpack}/portal/install"
route := "/mods/packs/test/portal/install"
handlerFunc := ModPackModPortalInstallHandler
t.Run("success", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
requestBody := strings.NewReader(`{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusOK, expected)
})
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
ModPackInvalidJsonBodyTest(t, method, baseRoute, route, handlerFunc)
t.Run("wrong download link", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
requestBody := strings.NewReader(`{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
})
}
func TestModPackModPortalInstallMultipleHandler(t *testing.T) {
CheckShort(t)
method := "POST"
baseRoute := "/mods/packs/{modpack}/portal/install/multiple"
route := "/mods/packs/test/portal/install/multiple"
handlerFunc := ModPackModPortalInstallMultipleHandler
t.Run("success", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
requestBody := strings.NewReader(`[{"name": "belt-balancer", "version": "2.1.2"}, {"name": "train-station-overview", "version": "2.0.2"}]`)
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.2","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusOK, expected)
})
t.Run("unknown mod", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
requestBody := strings.NewReader(`[{"name": "askdhcb", "version": "2.1.2"}]`)
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
})
t.Run("unknown version", func(t *testing.T) {
SetupModPacks(t, false, true)
defer CleanupModPacks(t)
requestBody := strings.NewReader(`[{"name": "belt-balancer", "version": "0.1.12"}]`)
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
})
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
ModPackInvalidJsonBodyTest(t, method, baseRoute, route, handlerFunc)
}

View File

@ -0,0 +1,223 @@
package api
import (
"fmt"
"github.com/gorilla/mux"
"github.com/mroote/factorio-server-manager/factorio"
"log"
"net/http"
)
func ModPortalListModsHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var statusCode int
resp, err, statusCode = factorio.ModPortalList()
w.WriteHeader(statusCode)
if err != nil {
resp = fmt.Sprintf("Error in listing mods from mod portal: %s\nresponse: %+v", err, resp)
log.Println(resp)
return
}
}
// ModPortalModInfoHandler returns JSON response with the mod details
func ModPortalModInfoHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
vars := mux.Vars(r)
modId := vars["mod"]
var statusCode int
resp, err, statusCode = factorio.ModPortalModDetails(modId)
if err != nil {
resp = fmt.Sprintf("Error in getting mod details from mod portal: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(statusCode)
}
func ModPortalInstallHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
// Get Data out of the request
var data struct {
DownloadURL string `json:"downloadUrl"`
Filename string `json:"fileName"`
ModName string `json:"modName"`
}
err = ReadFromRequestBody(w, r, &resp, &data)
if err != nil {
return
}
mods, err := CreateNewMods(w, &resp)
if err != nil {
return
}
err = mods.DownloadMod(data.DownloadURL, data.Filename, data.ModName)
if err != nil {
resp = fmt.Sprintf("Error downloading a mod: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = mods.ListInstalledMods()
}
func ModPortalLoginHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var data struct {
Username string `json:"username"`
Password string `json:"password"`
}
err = ReadFromRequestBody(w, r, &resp, &data)
if err != nil {
return
}
err, statusCode := factorio.FactorioLogin(data.Username, data.Password)
w.WriteHeader(statusCode)
if err != nil {
resp = fmt.Sprintf("Error trying to login into Factorio: %s", err)
log.Println(resp)
return
}
}
func ModPortalLoginStatusHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
var credentials factorio.Credentials
resp, err = credentials.Load()
if err != nil {
resp = fmt.Sprintf("Error getting the factorio credentials: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func ModPortalLogoutHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
var credentials factorio.Credentials
err = credentials.Del()
if err != nil {
resp = fmt.Sprintf("Error on logging out of factorio: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = false
}
func ModPortalInstallMultipleHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var data []struct {
Name string `json:"name"`
Version factorio.Version `json:"version"`
}
err = ReadFromRequestBody(w, r, &resp, &data)
if err != nil {
return
}
modList, err := CreateNewMods(w, &resp)
if err != nil {
return
}
for _, datum := range data {
// skip base mod because it is already included in factorio
if datum.Name == "base" {
continue
}
details, err, statusCode := factorio.ModPortalModDetails(datum.Name)
if err != nil || statusCode != http.StatusOK {
resp = fmt.Sprintf("Error in getting mod details from mod portal: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
//find correct mod-version
var found = false
for _, release := range details.Releases {
if release.Version.Equals(datum.Version) {
found = true
err := modList.DownloadMod(release.DownloadURL, release.FileName, details.Name)
if err != nil {
resp = fmt.Sprintf("Error downloading mod {%s}, error: %s", details.Name, err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
break
}
}
if !found {
log.Printf("Error downloading mod {%s}, error: %s", details.Name, "version not found")
w.WriteHeader(http.StatusInternalServerError)
}
}
resp = modList.ListInstalledMods()
}

View File

@ -0,0 +1,80 @@
package api
import (
"net/http"
"strings"
"testing"
)
func TestModPortalInstallHandler(t *testing.T) {
CheckShort(t)
method := "POST"
route := "/api/mods/portal/install"
handlerFunc := ModPortalInstallHandler
t.Run("success", func(t *testing.T) {
SetupMods(t, true)
defer CleanupMods(t)
requestBody := strings.NewReader(`{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, expected)
})
ModEmptyBodyTest(t, method, route, handlerFunc)
ModInvalidJsonTest(t, method, route, handlerFunc)
t.Run("wrong download link", func(t *testing.T) {
SetupMods(t, true)
defer CleanupMods(t)
requestBody := strings.NewReader(`{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
})
}
func TestModPortalInstallMultipleHandler(t *testing.T) {
CheckShort(t)
method := "POST"
route := "/api/mods/portal/install/multiple"
handlerFunc := ModPortalInstallMultipleHandler
t.Run("success", func(t *testing.T) {
SetupMods(t, true)
defer CleanupMods(t)
requestBody := strings.NewReader(`[{"name": "belt-balancer", "version": "2.1.2"}, {"name": "train-station-overview", "version": "2.0.2"}]`)
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.2","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, expected)
})
t.Run("unknown mod", func(t *testing.T) {
SetupMods(t, true)
defer CleanupMods(t)
requestBody := strings.NewReader(`[{"name": "askdhcb", "version": "2.1.2"}]`)
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
})
t.Run("unknown version", func(t *testing.T) {
SetupMods(t, true)
defer CleanupMods(t)
requestBody := strings.NewReader(`[{"name": "belt-balancer", "version": "0.1.12"}]`)
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
})
ModEmptyBodyTest(t, method, route, handlerFunc)
ModInvalidJsonTest(t, method, route, handlerFunc)
}

339
src/api/mods_handler.go Normal file
View File

@ -0,0 +1,339 @@
package api
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"
)
func CreateNewMods(w http.ResponseWriter, resp *interface{}) (modList factorio.Mods, 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)
w.WriteHeader(http.StatusInternalServerError)
}
return
}
func ReadFromRequestBody(w http.ResponseWriter, r *http.Request, resp *interface{}, data interface{}) (err error) {
//Get Data out of the request
body, err := ReadRequestBody(w, r, resp)
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)
w.WriteHeader(http.StatusBadRequest)
return
}
return
}
// Returns JSON response of all mods installed in factorio/mods
func ListInstalledModsHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
modList, err := CreateNewMods(w, &resp)
if err != nil {
return
}
resp = modList.ListInstalledMods().ModsResult
}
func ModToggleHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var data struct {
Name string `json:"name"`
}
err = ReadFromRequestBody(w, r, &resp, &data)
if err != nil {
return
}
mods, err := CreateNewMods(w, &resp)
if err != nil {
return
}
err, resp = mods.ModSimpleList.ToggleMod(data.Name)
if err != nil {
resp = fmt.Sprintf("Error in toggling mod in simple list: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func ModDeleteHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
var data struct {
Name string `json:"name"`
}
// Get Data out of the request
err = ReadFromRequestBody(w, r, &resp, &data)
if err != nil {
return
}
modList, err := CreateNewMods(w, &resp)
if err != nil {
return
}
err = modList.DeleteMod(data.Name)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp = fmt.Sprintf("Error in deleting mod {%s}: %s", data.Name, err)
log.Println(resp)
return
}
resp = data.Name
}
func ModDeleteAllHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//delete mods folder
err = factorio.DeleteAllMods()
if err != nil {
resp = fmt.Sprintf("Error deleting all mods: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = nil
}
func ModUpdateHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//Get Data out of the request
var modData struct {
Name string `json:"modName"`
DownloadUrl string `json:"downloadUrl"`
Filename string `json:"fileName"`
}
err = ReadFromRequestBody(w, r, &resp, &modData)
if err != nil {
return
}
mods, err := CreateNewMods(w, &resp)
if err != nil {
return
}
err = mods.UpdateMod(modData.Name, modData.DownloadUrl, modData.Filename)
if err != nil {
resp = fmt.Sprintf("Error updating mod {%s}: %s", modData.Name, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
installedMods := mods.ListInstalledMods().ModsResult
for _, mod := range installedMods {
if mod.Name == modData.Name {
resp = mod
return
}
}
resp = fmt.Sprintf(`Could not find mod %s`, modData.Name)
log.Println(resp)
w.WriteHeader(http.StatusNotFound)
return
}
func ModUploadHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
formFile, fileHeader, err := r.FormFile("mod_file")
if err != nil {
resp = fmt.Sprintf("error getting uploaded file: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusBadRequest)
return
}
defer formFile.Close()
mods, err := CreateNewMods(w, &resp)
if err != nil {
return
}
err = mods.UploadMod(formFile, fileHeader)
if err != nil {
resp = fmt.Sprintf("error saving file to mods: %s", err)
log.Println(resp)
w.WriteHeader(http.StatusInternalServerError)
return
}
resp = mods.ListInstalledMods()
}
func ModDownloadHandler(w http.ResponseWriter, r *http.Request) {
var err error
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
config := bootstrap.GetConfig()
//iterate over folder and create everything in the zip
err = filepath.Walk(config.FactorioModsDir, func(path string, info os.FileInfo, err error) error {
if info.IsDir() == false {
//Lock the file, that we are want to read
err := factorio.FileLock.RLock(path)
if err != nil {
log.Printf("error locking file for reading, something else has locked it")
return err
}
defer factorio.FileLock.RUnlock(path)
writer, err := zipWriter.Create(info.Name())
if err != nil {
log.Printf("error on creating new file inside zip: %s", err)
return err
}
file, err := os.Open(path)
if err != nil {
log.Printf("error on opening modfile: %s", err)
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
if err != nil {
log.Printf("error on copying file into zip: %s", err)
return err
}
err = file.Close()
if err != nil {
log.Printf("error closing file: %s", err)
return err
}
}
return nil
})
if err == lockfile.ErrorAlreadyLocked {
w.WriteHeader(http.StatusLocked)
return
}
if err != nil {
log.Printf("error on walking over the mods: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
writerHeader := w.Header()
writerHeader.Set("Content-Type", "application/zip;charset=UTF-8")
writerHeader.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", "all_installed_mods.zip"))
}
//LoadModsFromSaveHandler returns JSON response with the found mods
func LoadModsFromSaveHandler(w http.ResponseWriter, r *http.Request) {
var err error
var resp interface{}
defer func() {
WriteResponse(w, resp)
}()
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//Get Data out of the request
var saveFileStruct struct {
Name string `json:"saveFile"`
}
err = ReadFromRequestBody(w, r, &resp, &saveFileStruct)
if err != nil {
return
}
config := bootstrap.GetConfig()
path := filepath.Join(config.FactorioSavesDir, saveFileStruct.Name)
f, err := factorio.OpenArchiveFile(path, "level.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)
return
}
defer f.Close()
var header factorio.SaveHeader
err = header.ReadFrom(f)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp = fmt.Sprintf("cannot read save header: %v", err)
log.Println(resp)
return
}
resp = header
}

View File

@ -0,0 +1,486 @@
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"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestMain(m *testing.M) {
var err error
godotenv.Load("../.env")
// basic setup stuff
bootstrap.NewConfig([]string{
"--dir", os.Getenv("dir"),
"--conf", os.Getenv("conf"),
"--mod-pack-dir", os.Getenv("mod_pack_dir"),
"--mod-dir", os.Getenv("mod_dir"),
})
factorio.SetFactorioServer(factorio.Server{
Version: factorio.Version{0, 18, 30, 0},
BaseModVersion: "0.18.30",
})
// check login status
var cred factorio.Credentials
load, err := cred.Load()
if err != nil {
log.Fatalf("Error loading factorio credentials: %s", err)
return
}
if !load {
// no credentials found, login...
err, _ = factorio.FactorioLogin(os.Getenv("factorio_username"), os.Getenv("factorio_password"))
if err != nil {
log.Printf("Error logging in into factorio: %s", err)
}
}
os.Exit(m.Run())
}
func CheckShort(t *testing.T) {
if testing.Short() {
t.Skip("Do not run in Short-mode")
}
}
func SetupMods(t *testing.T, empty bool) {
var err error
config := bootstrap.GetConfig()
// check if dev directory exists and create it
if _, err = os.Stat(config.FactorioModsDir); os.IsNotExist(err) {
err = os.Mkdir(config.FactorioModsDir, 0775)
}
if err != nil {
log.Fatalf(`Error creating "dev" directory: %s`, err)
return
}
mod, err := factorio.NewMods(config.FactorioModsDir)
if err != nil {
t.Fatalf("couldn't create Mods object: %s", err)
}
if !empty {
err := mod.DownloadMod("/download/belt-balancer/5e9f9db4bf9d30000c5303f2", "belt-balancer_2.1.3.zip", "belt-balancer")
if err != nil {
t.Fatalf(`Error downloading Mod "belt-balancer": %s`, err)
}
err = mod.DownloadMod("/download/train-station-overview/5e8a0a8ee8864f000d0cb022", "train-station-overview_2.0.3.zip", "train-station-overview")
if err != nil {
t.Fatalf(`Error downloading Mod "train-station-overview": %s`, err)
}
}
}
func CleanupMods(t *testing.T) {
config := bootstrap.GetConfig()
err := os.RemoveAll(config.FactorioModsDir)
if err != nil {
t.Fatalf("Error removing dev directory: %s", err)
}
}
func CallRoute(t *testing.T, method string, baseRoute string, route string, body io.Reader, handlerFunc http.HandlerFunc, statusCode int, expected string) {
// create request to send
request, err := http.NewRequest(method, route, body)
if err != nil {
t.Fatalf("Error creating request: %s", err)
}
// create response recorder
recorder := httptest.NewRecorder()
// get the handler, where the request is handled
router := mux.NewRouter()
router.HandleFunc(baseRoute, handlerFunc)
// call the handler directly
router.ServeHTTP(recorder, request)
//handler.ServeHTTP(recorder, request)
// status has to be 200
if recorder.Code != statusCode {
t.Fatalf("Wrong Status Code. expected %v - got %v", statusCode, recorder.Code)
}
if expected != "" {
actual := recorder.Body.String()
require.JSONEqf(t, expected, actual, `Wrong Body for route "%s". expected "%v" - actual "%v"`, route, expected, actual)
}
}
func ModEmptyBodyTest(t *testing.T, method string, route string, handlerFunc http.HandlerFunc) {
t.Run("empty body", func(t *testing.T) {
SetupMods(t, true)
defer CleanupMods(t)
CallRoute(t, method, route, route, nil, handlerFunc, http.StatusBadRequest, "")
})
}
func ModInvalidJsonTest(t *testing.T, method, route string, handlerFunc http.HandlerFunc) {
t.Run("invalid json body", func(t *testing.T) {
SetupMods(t, true)
defer CleanupMods(t)
requestBody := strings.NewReader(`{"name": "asdc"`)
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusBadRequest, "")
})
}
func ModNotExistTest(t *testing.T, method, route string, handlerFunc http.HandlerFunc) {
t.Run("mod not exist", func(t *testing.T) {
requestBody := strings.NewReader(`{"name": "lasdg"}`)
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
})
}
func TestListInstalledModsHandler(t *testing.T) {
CheckShort(t)
SetupMods(t, false)
defer CleanupMods(t)
route := "/api/mods/list"
expected := `[{"name":"belt-balancer","version":"2.1.3","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.3","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]`
CallRoute(t, "GET", route, route, nil, ListInstalledModsHandler, http.StatusOK, expected)
}
func TestModToggleHandler(t *testing.T) {
CheckShort(t)
method := "POST"
route := "/api/mods/toggle"
handlerFunc := ModToggleHandler
t.Run("success", func(t *testing.T) {
SetupMods(t, false)
defer CleanupMods(t)
requestBody := strings.NewReader(`{"name": "belt-balancer"}`)
// mod is now deactivated
expected := "false"
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, expected)
// check if changes happenes
config := bootstrap.GetConfig()
modList, err := factorio.NewMods(config.FactorioModsDir)
if err != nil {
t.Fatalf("Error creating Mods object: %s", err)
}
found := false
for _, mod := range modList.ModSimpleList.Mods {
if mod.Name == "belt-balancer" {
// this mod has to be deactivated now
if mod.Enabled {
t.Fatalf("Mod is wrongly enabled, it should be disabled by now")
}
found = true
break
}
}
if !found {
t.Fatalf("Mod not found")
}
// toggle again, to check if the other direction also works
// mod is now activated again
expected = "true"
// reset request body, it has to be red again
requestBody.Seek(0, 0)
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, expected)
modList, err = factorio.NewMods(config.FactorioModsDir)
if err != nil {
t.Fatalf("Error creating Mods object: %s", err)
}
found = false
for _, mod := range modList.ModSimpleList.Mods {
if mod.Name == "belt-balancer" {
// this mod has to be deactivated now
if !mod.Enabled {
t.Fatalf("Mod is wrongly disabled, it should be enabled again")
}
found = true
break
}
}
if !found {
t.Fatalf("Mod not found")
}
})
ModEmptyBodyTest(t, method, route, handlerFunc)
ModInvalidJsonTest(t, method, route, handlerFunc)
ModNotExistTest(t, method, route, handlerFunc)
}
func TestModDeleteHandler(t *testing.T) {
CheckShort(t)
method := "POST"
route := "/api/mods/delete"
handlerFunc := ModDeleteHandler
t.Run("success", func(t *testing.T) {
SetupMods(t, false)
defer CleanupMods(t)
requestBody := strings.NewReader(`{"name": "belt-balancer"}`)
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, `"belt-balancer"`)
// check if mod is really not installed anymore
config := bootstrap.GetConfig()
modList, err := factorio.NewMods(config.FactorioModsDir)
if err != nil {
t.Fatalf("Error creating Mods object: %s", err)
}
if modList.ModSimpleList.CheckModExists("belt-balancer") {
t.Fatalf("Mod is still installed, it should be gone by now")
}
})
ModEmptyBodyTest(t, method, route, handlerFunc)
ModInvalidJsonTest(t, method, route, handlerFunc)
ModNotExistTest(t, method, route, handlerFunc)
}
func TestModDeleteAllHandler(t *testing.T) {
CheckShort(t)
method := "POST"
route := "/api/mods/delete/all"
handlerFunc := ModDeleteAllHandler
t.Run("success", func(t *testing.T) {
SetupMods(t, false)
defer CleanupMods(t)
CallRoute(t, method, route, route, nil, handlerFunc, http.StatusOK, "null")
// check if no mods are there
config := bootstrap.GetConfig()
modList, err := factorio.NewMods(config.FactorioModsDir)
if err != nil {
t.Fatalf("Error creating mods object: %s", err)
}
if len(modList.ListInstalledMods().ModsResult) != 0 {
t.Fatalf("Mods are still there!")
}
})
}
func TestModUpdateHandler(t *testing.T) {
CheckShort(t)
method := "POST"
route := "/api/mods/update"
handlerFunc := ModUpdateHandler
requestBodySuccess := `{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`
t.Run("success", func(t *testing.T) {
SetupMods(t, false)
defer CleanupMods(t)
expected := `{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}`
CallRoute(t, method, route, route, strings.NewReader(requestBodySuccess), handlerFunc, http.StatusOK, expected)
})
t.Run("success with disabled mod", func(t *testing.T) {
SetupMods(t, false)
defer CleanupMods(t)
// disable "belt-balancer" mod, so we can test, if it is still deactivated after
config := bootstrap.GetConfig()
modList, err := factorio.NewMods(config.FactorioModsDir)
if err != nil {
t.Fatalf("Error creating mods object: %s", err)
}
err, _ = modList.ModSimpleList.ToggleMod("belt-balancer")
if err != nil {
t.Fatalf("Error toggling mod: %s", err)
}
expected := `{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":false}`
CallRoute(t, method, route, route, strings.NewReader(requestBodySuccess), handlerFunc, http.StatusOK, expected)
})
ModEmptyBodyTest(t, method, route, handlerFunc)
ModInvalidJsonTest(t, method, route, handlerFunc)
t.Run("mod not exist", func(t *testing.T) {
SetupMods(t, false)
defer CleanupMods(t)
requestBody := strings.NewReader(`{"modName": "alfbasd", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusNotFound, "")
})
t.Run("downloadUrl invalid", func(t *testing.T) {
SetupMods(t, false)
defer CleanupMods(t)
requestBody := strings.NewReader(`{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
})
}
func ModUploadRequest(t *testing.T, body bool, filePath string) *httptest.ResponseRecorder {
CheckShort(t)
var err error
method := "POST"
route := "/api/mods/upload"
handlerFunc := ModUploadHandler
requestBody := &bytes.Buffer{}
writer := multipart.NewWriter(requestBody)
if body {
file, err := os.Open(filePath)
if err == nil {
assert.NoError(t, err, "error opening mod file")
formFile, err := writer.CreateFormFile("mod_file", filepath.Base(filePath))
assert.NoError(t, err, "error creating formFileWriter")
_, err = io.Copy(formFile, file)
assert.NoError(t, err, "error copying file to form")
}
}
err = writer.Close()
if err != nil {
t.Fatalf("error closing the multipart writer: %s", err)
}
// create request to send
request, err := http.NewRequest(method, route, requestBody)
assert.NoError(t, err, "Error creating request")
request.Header.Set("Content-Type", writer.FormDataContentType())
// create response recorder
recorder := httptest.NewRecorder()
// get the handler, where the request is handled
handler := http.HandlerFunc(handlerFunc)
// call the handler directly
handler.ServeHTTP(recorder, request)
return recorder
}
func TestModUploadHandler(t *testing.T) {
CheckShort(t)
method := "POST"
route := "/api/mods/upload"
handlerFunc := ModUploadHandler
t.Run("success", func(t *testing.T) {
SetupMods(t, true)
defer CleanupMods(t)
recorder := ModUploadRequest(t, true, "../factorio_testfiles/belt-balancer_2.1.3.zip")
// status has to be 200
if recorder.Code != http.StatusOK {
t.Fatalf("Wrong Status Code. expected %v - got %v", http.StatusOK, recorder.Code)
}
// check if mod is uploaded correctly
config := bootstrap.GetConfig()
modList, err := factorio.NewMods(config.FactorioModsDir)
assert.NoError(t, err, "error creating mods object")
expected := factorio.ModsResultList{
ModsResult: []factorio.ModsResult{
{
ModInfo: factorio.ModInfo{
Name: "belt-balancer",
Version: "2.1.3",
Title: "Belt Balancer",
Author: "knoxfighter",
FileName: "belt-balancer_2.1.3.zip",
FactorioVersion: factorio.Version{0, 18, 0, 0},
Dependencies: nil,
Compatibility: true,
},
Enabled: true,
},
},
}
actual := modList.ListInstalledMods()
assert.Equal(t, expected, actual, `New mod is not correctly installed. expected "%v" - actual "%v"`, expected, actual)
})
ModEmptyBodyTest(t, method, route, handlerFunc)
t.Run("empty file", func(t *testing.T) {
SetupMods(t, true)
defer CleanupMods(t)
recorder := ModUploadRequest(t, true, "")
assert.Equal(t, http.StatusBadRequest, recorder.Code, "wrong response code.")
})
t.Run("invalid mod file (txt-file)", func(t *testing.T) {
SetupMods(t, true)
defer CleanupMods(t)
recorder := ModUploadRequest(t, false, "../factorio_testfiles/file_usage.txt")
assert.Equal(t, http.StatusBadRequest, recorder.Code, "wrong response code.")
})
t.Run("invalid mod file (zip-file)", func(t *testing.T) {
SetupMods(t, true)
defer CleanupMods(t)
recorder := ModUploadRequest(t, true, "../factorio_testfiles/invalid_mod.zip")
assert.Equal(t, http.StatusInternalServerError, recorder.Code, "wrong response code.")
})
}

View File

@ -1,22 +1,13 @@
package main
package api
import (
"github.com/mroote/factorio-server-manager/api/websocket"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
//TODO Proper origin check
CheckOrigin: func(r *http.Request) bool { return true },
}
type Handler func(*Client, interface{})
type Route struct {
Name string
Method string
@ -26,13 +17,8 @@ type Route struct {
type Routes []Route
type WSRouter struct {
rules map[string]Handler
}
func NewRouter() *mux.Router {
r := mux.NewRouter().StrictSlash(true)
ws := NewWSRouter()
// API subrouter
// Serves all JSON REST handlers prefixed with /api
@ -57,9 +43,15 @@ func NewRouter() *mux.Router {
r.Path("/ws").
Methods("GET").
Name("Websocket").
Handler(AuthorizeHandler(ws))
ws.Handle("command send", commandSend)
ws.Handle("log subscribe", logSubscribe)
Handler(
AuthorizeHandler(
http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
websocket.ServeWs(w, r)
},
),
),
)
// Serves the frontend application from the app directory
// Uses basic file server to serve index.html and Javascript application
@ -68,34 +60,40 @@ func NewRouter() *mux.Router {
Methods("GET").
Name("Login").
Handler(http.StripPrefix("/login", http.FileServer(http.Dir("./app/"))))
r.Path("/settings").
Methods("GET").
Name("Settings").
Handler(AuthorizeHandler(http.StripPrefix("/settings", http.FileServer(http.Dir("./app/")))))
r.Path("/mods").
Methods("GET").
Name("Mods").
Handler(AuthorizeHandler(http.StripPrefix("/mods", http.FileServer(http.Dir("./app/")))))
r.Path("/saves").
Methods("GET").
Name("Saves").
Handler(AuthorizeHandler(http.StripPrefix("/saves", http.FileServer(http.Dir("./app/")))))
r.Path("/mods").
Methods("GET").
Name("Mods").
Handler(AuthorizeHandler(http.StripPrefix("/mods", http.FileServer(http.Dir("./app/")))))
r.Path("/server-settings").
Methods("GET").
Name("Server settings").
Handler(AuthorizeHandler(http.StripPrefix("/server-settings", http.FileServer(http.Dir("./app/")))))
r.Path("/game-settings").
Methods("GET").
Name("Game settings").
Handler(AuthorizeHandler(http.StripPrefix("/game-settings", http.FileServer(http.Dir("./app/")))))
r.Path("/console").
Methods("GET").
Name("Console").
Handler(AuthorizeHandler(http.StripPrefix("/console", http.FileServer(http.Dir("./app/")))))
r.Path("/logs").
Methods("GET").
Name("Logs").
Handler(AuthorizeHandler(http.StripPrefix("/logs", http.FileServer(http.Dir("./app/")))))
r.Path("/config").
r.Path("/user-management").
Methods("GET").
Name("Config").
Handler(AuthorizeHandler(http.StripPrefix("/config", http.FileServer(http.Dir("./app/")))))
r.Path("/server").
Name("User management").
Handler(AuthorizeHandler(http.StripPrefix("/user-management", http.FileServer(http.Dir("./app/")))))
r.Path("/help").
Methods("GET").
Name("Server").
Handler(AuthorizeHandler(http.StripPrefix("/server", http.FileServer(http.Dir("./app/")))))
r.Path("/console").
Methods("GET").
Name("Server").
Handler(AuthorizeHandler(http.StripPrefix("/console", http.FileServer(http.Dir("./app/")))))
Name("Help").
Handler(AuthorizeHandler(http.StripPrefix("/help", http.FileServer(http.Dir("./app/")))))
// catch all route
r.PathPrefix("/").
Methods("GET").
Name("Index").
@ -108,6 +106,7 @@ func NewRouter() *mux.Router {
// 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)
@ -117,113 +116,10 @@ func AuthorizeHandler(h http.Handler) http.Handler {
})
}
func NewWSRouter() *WSRouter {
return &WSRouter{
rules: make(map[string]Handler),
}
}
func (ws *WSRouter) Handle(msgName string, handler Handler) {
ws.rules[msgName] = handler
}
func (ws *WSRouter) FindHandler(msgName string) (Handler, bool) {
handler, found := ws.rules[msgName]
return handler, found
}
func (ws *WSRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
socket, err := upgrader.Upgrade(w, r, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Error opening ws connection: %s", err)
return
}
client := NewClient(socket, ws.FindHandler)
defer client.Close()
go client.Write()
client.Read()
}
// Defines all API REST endpoints
// All routes are prefixed with /api
var apiRoutes = Routes{
Route{
"ListInstalledMods",
"GET",
"/mods/list/installed",
listInstalledModsHandler,
}, {
"LoginFactorioModPortal",
"POST",
"/mods/factorio/login",
LoginFactorioModPortal,
}, {
"LoginstatusFactorioModPortal",
"POST",
"/mods/factorio/status",
LoginstatusFactorioModPortal,
}, {
"LogoutFactorioModPortal",
"POST",
"/mods/factorio/logout",
LogoutFactorioModPortalHandler,
}, {
"SearchModPortal",
"GET",
"/mods/search",
ModPortalSearchHandler,
}, {
"GetModDetails",
"POST",
"/mods/details",
ModPortalDetailsHandler,
}, {
"ModPortalInstall",
"POST",
"/mods/install",
ModPortalInstallHandler,
}, {
"ModPortalInstallMultiple",
"POST",
"/mods/install/multiple",
ModPortalInstallMultipleHandler,
}, {
"ToggleMod",
"POST",
"/mods/toggle",
ToggleModHandler,
}, {
"DeleteMod",
"POST",
"/mods/delete",
DeleteModHandler,
}, {
"DeleteAllMods",
"POST",
"/mods/delete/all",
DeleteAllModsHandler,
}, {
"UpdateMod",
"POST",
"/mods/update",
UpdateModHandler,
}, {
"UploadMod",
"POST",
"/mods/upload",
UploadModHandler,
}, {
"DownloadMods",
"GET",
"/mods/download",
DownloadModsHandler,
}, {
"LoadModsFromSave",
"POST",
"/mods/save/load",
LoadModsFromSaveHandler,
}, {
{
"ListSaves",
"GET",
"/saves/list",
@ -248,6 +144,11 @@ var apiRoutes = Routes{
"GET",
"/saves/create/{save}",
CreateSaveHandler,
}, {
"LoadModsFromSave",
"POST",
"/saves/mods",
LoadModsFromSaveHandler,
}, {
"LogTail",
"GET",
@ -258,11 +159,6 @@ var apiRoutes = Routes{
"GET",
"/config",
LoadConfig,
}, {
"StartServer",
"GET",
"/server/start",
StartServer,
}, {
"StartServer",
"POST",
@ -313,46 +209,6 @@ var apiRoutes = Routes{
"POST",
"/user/remove",
RemoveUser,
}, {
"ListModPacks",
"GET",
"/mods/packs/list",
ListModPacksHandler,
}, {
"DownloadModPack",
"GET",
"/mods/packs/download/{modpack}",
DownloadModPackHandler,
}, {
"DeleteModPack",
"POST",
"/mods/packs/delete",
DeleteModPackHandler,
}, {
"CreateModPack",
"POST",
"/mods/packs/create",
CreateModPackHandler,
}, {
"LoadModPack",
"POST",
"/mods/packs/load",
LoadModPackHandler,
}, {
"ModPackToggleMod",
"POST",
"/mods/packs/mod/toggle",
ModPackToggleModHandler,
}, {
"ModPackDeleteMod",
"POST",
"/mods/packs/mod/delete",
ModPackDeleteModHandler,
}, {
"ModPackUpdateMod",
"POST",
"/mods/packs/mod/update",
ModPackUpdateModHandler,
}, {
"GetServerSettings",
"GET",
@ -364,4 +220,147 @@ var apiRoutes = Routes{
"/settings/update",
UpdateServerSettings,
},
// Mod Portal Stuff
{
"ModPortalListAllMods",
"GET",
"/mods/portal/list",
ModPortalListModsHandler,
}, {
"ModPortalGetModInfo",
"GET",
"/mods/portal/info/{mod}",
ModPortalModInfoHandler,
}, {
"ModPortalInstallMod",
"POST",
"/mods/portal/install",
ModPortalInstallHandler,
}, {
"ModPortalLogin",
"POST",
"/mods/portal/login",
ModPortalLoginHandler,
}, {
"ModPortalLoginStatus",
"GET",
"/mods/portal/loginstatus",
ModPortalLoginStatusHandler,
}, {
"ModPortalLogout",
"GET",
"/mods/portal/logout",
ModPortalLogoutHandler,
}, {
"ModPortalInstallMultiple",
"POST",
"/mods/portal/install/multiple",
ModPortalInstallMultipleHandler,
},
// Mods Stuff
{
"ListInstalledMods",
"GET",
"/mods/list",
ListInstalledModsHandler,
}, {
"ToggleMod",
"POST",
"/mods/toggle",
ModToggleHandler,
}, {
"DeleteMod",
"POST",
"/mods/delete",
ModDeleteHandler,
}, {
"DeleteAllMods",
"POST",
"/mods/delete/all",
ModDeleteAllHandler,
}, {
"UpdateMod",
"POST",
"/mods/update",
ModUpdateHandler,
}, {
"UploadMod",
"POST",
"/mods/upload",
ModUploadHandler,
}, {
"DownloadMods",
"GET",
"/mods/download",
ModDownloadHandler,
},
// Mod Packs
{
"ModPacksList",
"GET",
"/mods/packs/list",
ModPackListHandler,
}, {
"ModPackCreate",
"POST",
"/mods/packs/create",
ModPackCreateHandler,
}, {
"ModPackDelete",
"POST",
"/mods/packs/{modpack}/delete",
ModPackDeleteHandler,
}, {
"ModPackDownload",
"GET",
"/mods/packs/{modpack}/download",
ModPackDownloadHandler,
}, {
"LoadModPack",
"POST",
"/mods/packs/{modpack}/load",
ModPackLoadHandler,
},
// Mods inside Mod Packs
{
"ModPackListMods",
"GET",
"/mods/packs/{modpack}/list",
ModPackModListHandler,
}, {
"ModPackToggleMod",
"POST",
"/mods/packs/{modpack}/mod/toggle",
ModPackModToggleHandler,
}, {
"ModPackDeleteMod",
"POST",
"/mods/packs/{modpack}/mod/delete",
ModPackModDeleteHandler,
}, {
"ModPackDeleteAllMod",
"POST",
"/mods/packs/{modpack}/mod/delete/all",
ModPackModDeleteAllHandler,
}, {
"ModPackUpdateMod",
"POST",
"/mods/packs/{modpack}/mod/update",
ModPackModUpdateHandler,
}, {
"ModPackUploadMod",
"POST",
"/mods/packs/{modpack}/mod/upload",
ModPackModUploadHandler,
}, {
"ModPackModPortalInstallMod",
"POST",
"/mods/packs/{modpack}/portal/install",
ModPackModPortalInstallHandler,
}, {
"ModPackModPortalInstallMultiple",
"POST",
"/mods/packs/{modpack}/portal/install/multiple",
ModPackModPortalInstallMultipleHandler,
},
}

View File

@ -0,0 +1,171 @@
package websocket
import (
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
// Timeout for sending a message
writeWait = 10 * time.Second
// Timeout between the answer of two pong messages
pongWait = 60 * time.Second
// Period in which a new ping message is sent. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size sent from a client
maxMessageSize = 2048
)
// The upgrader to upgrade from http to ws protocol
var upgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
}
// The websocket client, that is the middleman between a websocket connection and the hub.
// It manages every communication between the hub and the websocket connection.
type wsClient struct {
// The hub this client is registered to.
hub *wsHub
// The websocket connection.
conn *websocket.Conn
// channel to send messages to the websocket connection.
send chan wsMessage
}
// read messages from the websocket connection, choose what has to be done with it and execute that action
// messages with room name, send to the room
// messages with empty room name and controls set, will execute that control, nothing will be sent to other clients
// messages with empty room name and empty controls will send the message to all clients registered in the hub.
//
// This pump has to be executed in a goroutine!
func (client *wsClient) readPump() {
// When this pump closes, unregister the client and close the websocket connection
defer func() {
client.hub.unregister <- client
client.conn.Close()
}()
// Setup some websocket connection settings
client.conn.SetReadLimit(maxMessageSize)
client.conn.SetReadDeadline(time.Now().Add(pongWait))
client.conn.SetPongHandler(func(string) error {
client.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
// wait and read the next incoming message on the websocket.
var message wsMessage
err := client.conn.ReadJSON(&message)
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}
if message.RoomName == "" {
// controls messages will not sent to other clients, they only are relevant for the server
if message.Controls != (WsControls{}) {
// this message is a control message, do its job!
switch message.Controls.Type {
case "subscribe":
room := client.hub.GetRoom(message.Controls.Value)
room.register <- client
case "unsubscribe":
room := client.hub.GetRoom(message.Controls.Value)
room.unregister <- client
default:
for _, handler := range client.hub.controlHandlers {
go handler(message.Controls)
}
}
} else {
client.hub.broadcast <- message
}
} else {
// Send the message to the defined room
room := client.hub.GetRoom(message.RoomName)
room.send <- message
}
}
}
// write message to the websocket connection.
// messages from client.send channel are sent
// Also starts a timer ticker to send ping messages
//
// This pump has to be executed in a goroutine!
func (client *wsClient) writePump() {
// setup ping message ticker
ticker := time.NewTicker(pingPeriod)
// stop the ticker and close the websocket connection, when this pump is finished
defer func() {
ticker.Stop()
client.conn.Close()
}()
for {
select {
case message, ok := <-client.send:
// Setup timeout
client.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// The hub closed the channel. Therefore notify the client.
client.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
// send the message as json
err := client.conn.WriteJSON(message)
if err != nil {
return
}
case <-ticker.C:
// Setup timeout
client.conn.SetWriteDeadline(time.Now().Add(writeWait))
// send a ping message
if err := client.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// serveWs is the http handler to upgrade from http to ws..
// Also the startup point for a client
func ServeWs(w http.ResponseWriter, r *http.Request) {
// upgrade the connection
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
// setup the client
client := &wsClient{
hub: WebsocketHub,
conn: conn,
send: make(chan wsMessage, 256),
}
// register this client in the hub
client.hub.register <- client
// start the pipes for the new client in goroutines
go client.writePump()
go client.readPump()
}

231
src/api/websocket/wshub.go Normal file
View File

@ -0,0 +1,231 @@
package websocket
import (
"github.com/mroote/factorio-server-manager/bootstrap"
"reflect"
)
// the hub, that is exported and can be used anywhere to work with the websocket
var WebsocketHub *wsHub
var LogCache []string
// a controlHandler is used to determine, if something has to be done, on a specific command.
// register a handler with `wsHub.RegisterControlHandler`
// unregister a handler with `wsHub.UnregisterControlHandler`
type controlHandler func(controls WsControls)
// The type for of control messages.
// Type ans Value both have to be set, if controls are sent.
// Currently supported Type:
// - `subscribe` - Value used as room name
// - `unsubscribe` - Value used as room name
// - `command` - Value contains the command to execute
type WsControls struct {
Type string `json:"type"`
Value string `json:"value"`
}
// the main message of our websocket protocol.
// if the room_name is an empty string, this will be sent as broadcast
// if controls is not empty, this will not be sent anywhere, but used as commands, to join/leave rooms and to send commands to the factorio server
type wsMessage struct {
RoomName string `json:"room_name"`
Message interface{} `json:"message,omitempty"`
Controls WsControls `json:"controls,omitempty"`
}
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
// register a client to this room
register chan *wsClient
// unregister a client from this room, if no clients remain, this room will be deleted
unregister chan *wsClient
// send a message to all clients in this room
send chan wsMessage
}
// Hub is the basic setup of the server.
// It contains everything needed for the websocket to run.
// Only the controlHandler Subscriptions are public, everything else can be controlled with the functions and the wsClient.
type wsHub struct {
// list of all connected clients
clients map[*wsClient]bool
// Messages that should be sent to ALL clients
broadcast chan wsMessage
// a list of all rooms
rooms map[string]*wsRoom
// register a client to this hub
register chan *wsClient
// unregister a client from this hub
unregister chan *wsClient
// run a control message on all registered controlHandler
runControl chan WsControls
// list of all registered controlHandlers
controlHandlers map[reflect.Value]controlHandler
// register a controlHandler
RegisterControlHandler chan controlHandler
// unregister a controlHandler
UnregisterControlHandler chan controlHandler
}
// initialize and run the mein websocket hub.
func init() {
WebsocketHub = &wsHub{
broadcast: make(chan wsMessage),
register: make(chan *wsClient),
rooms: make(map[string]*wsRoom),
unregister: make(chan *wsClient),
clients: make(map[*wsClient]bool),
runControl: make(chan WsControls),
controlHandlers: make(map[reflect.Value]controlHandler),
RegisterControlHandler: make(chan controlHandler),
UnregisterControlHandler: make(chan controlHandler),
}
go WebsocketHub.run()
}
// remove a client from this hub and all of its rooms
func (hub *wsHub) removeClient(client *wsClient) {
delete(hub.clients, client)
close(client.send)
for _, room := range hub.rooms {
room.unregister <- client
}
}
// run starts a websocket hub, this has to be done in a subroutine `go hub.run()`
func (hub *wsHub) run() {
for {
select {
case client := <-hub.register:
hub.clients[client] = true
case client := <-hub.unregister:
if _, ok := hub.clients[client]; ok {
hub.removeClient(client)
}
case message := <-hub.broadcast:
for client := range hub.clients {
select {
case client.send <- message:
default:
hub.removeClient(client)
}
}
case function := <-hub.RegisterControlHandler:
hub.controlHandlers[reflect.ValueOf(function)] = function
case function := <-hub.UnregisterControlHandler:
delete(hub.controlHandlers, reflect.ValueOf(function))
}
}
}
// Broadcast a message to all connected clients (only clients connected to this room).
func (hub *wsHub) Broadcast(message interface{}) {
hub.broadcast <- wsMessage{
RoomName: "",
Message: message,
}
}
// get a websocket room or create it, if it doesn't exist yet.
// Also starts the rooms subroutine `wsRoom.run()`
func (hub *wsHub) GetRoom(name string) *wsRoom {
if room, ok := hub.rooms[name]; ok {
return room
} else {
room := &wsRoom{
name: name,
hub: hub,
clients: make(map[*wsClient]bool),
register: make(chan *wsClient),
unregister: make(chan *wsClient),
send: make(chan wsMessage),
}
hub.rooms[name] = room
go room.run()
return room
}
}
// run starts a websocket room. This has to be run as a subroutine `go room.run()`
func (room *wsRoom) run() {
for {
select {
case client := <-room.register:
room.clients[client] = true
// some hardcoded stuff for gamelog room
if room.name == "gamelog" {
// send cached log to registered client
for _, logLine := range LogCache {
client.send <- wsMessage{
RoomName: "gamelog",
Message: logLine,
}
}
}
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
}
}
case message := <-room.send:
for client := range room.clients {
select {
case client.send <- message:
default:
room.unregister <- client
}
}
// some hardcoded stuff for gamelog room
if room.name == "gamelog" {
// add the line to the cache
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:]
}
}
}
}
}
// Send a message into this room.
func (room *wsRoom) Send(message interface{}) {
room.send <- wsMessage{
RoomName: room.name,
Message: message,
}
}

142
src/bootstrap/config.go Normal file
View File

@ -0,0 +1,142 @@
package bootstrap
import (
"encoding/json"
"fmt"
"github.com/jessevdk/go-flags"
"log"
"math/rand"
"os"
"path/filepath"
"runtime"
"time"
)
type Flags struct {
ConfFile string `long:"conf" default:"./conf.json" description:"Specify location of Factorio Server Manager config file."`
FactorioDir string `long:"dir" default:"./" description:"Specify location of Factorio directory."`
ServerIP string `long:"host" default:"0.0.0.0" description:"Specify IP for webserver to listen on."`
FactorioIP string `long:"game-bind-address" default:"0.0.0.0" description:"Specify IP for Fcatorio gamer server to listen on."`
FactorioPort string `long:"port" default:"80" description:"Specify a port for the server."`
FactorioConfigFile string `long:"config" default:"config/config.ini" description:"Specify location of Factorio config.ini file"`
FactorioMaxUpload int64 `long:"max-upload" default:"20.971.520" description:"Maximum filesize for uploaded files (default 20MB)."`
FactorioBinary string `long:"bin" default:"bin/x64/factorio" description:"Location of Factorio Server binary file"`
GlibcCustom string `long:"glibc-custom" default:"false" description:"By default false, if custom glibc is required set this to true and add glibc-loc and glibc-lib-loc parameters"`
GlibcLocation string `long:"glibc-loc" default:"/opt/glibc-2.18/lib/ld-2.18.so" description:"Location glibc ld.so file if needed (ex. /opt/glibc-2.18/lib/ld-2.18.so)"`
GlibcLibLoc string `long:"glibc-lib-loc" default:"/opt/glibc-2.18/lib" description:"Location of glibc lib folder (ex. /opt/glibc-2.18/lib)"`
Autostart string `long:"autostart" default:"false" description:"Autostart factorio server on bootup of FSM, default false [true/false]"`
ModPackDir string `long:"mod-pack-dir" default:"./mod_packs" description:"Directory to store mod packs."`
}
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"`
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
}
var instantiated Config
func NewConfig(args []string) Config {
var opts Flags
_, err := flags.NewParser(&opts, flags.IgnoreUnknown).ParseArgs(args)
if err != nil {
failOnError(err, "Failed to parse arguments")
}
instantiated = mapFlags(opts)
instantiated.loadServerConfig()
abs, err := filepath.Abs(instantiated.FactorioModPackDir)
println(abs)
return instantiated
}
func GetConfig() Config {
return instantiated
}
// 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)
failOnError(err, "Error loading config file.")
decoder := json.NewDecoder(file)
err = decoder.Decode(&config)
failOnError(err, "Error decoding JSON config file.")
config.FactorioRconPort = randomPort()
}
// Returns random port to use for rcon connection
func randomPort() int {
// rand needs to be initialized, else we always get the same number
rand.Seed(time.Now().UnixNano())
// get a random number between 40000 and 45000
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,
}
if runtime.GOOS == "windows" {
appdata := os.Getenv("APPDATA")
config.FactorioLog = filepath.Join(appdata, "Factorio", "factorio-current.log")
} else {
config.FactorioLog = filepath.Join(config.FactorioDir, "factorio-current.log")
}
return config
}
func failOnError(err error, msg string) {
if err != nil {
log.Printf("%s: %s", msg, err)
panic(fmt.Sprintf("%s: %s", msg, err))
}
}

View File

@ -1,4 +1,4 @@
package main
package factorio
import (
"log"
@ -6,8 +6,8 @@ import (
"github.com/go-ini/ini"
)
// Loads config.ini file from the factorio config directory
func loadConfig(filename string) (map[string]map[string]string, error) {
// Loads config.ini file from the factorio bootstrap directory
func LoadConfig(filename string) (map[string]map[string]string, error) {
log.Printf("Loading config file: %s", filename)
cfg, err := ini.Load(filename)
if err != nil {

View File

@ -0,0 +1,74 @@
package factorio
import (
"encoding/json"
"errors"
"github.com/mroote/factorio-server-manager/bootstrap"
"io/ioutil"
"log"
"os"
)
type Credentials struct {
Username string `json:"username"`
Userkey string `json:"userkey"`
}
func (credentials *Credentials) Save() error {
var err error
config := bootstrap.GetConfig()
credentialsJson, err := json.Marshal(credentials)
if err != nil {
log.Printf("error mashalling the credentials: %s", err)
return err
}
err = ioutil.WriteFile(config.FactorioCredentialsFile, credentialsJson, 0664)
if err != nil {
log.Printf("error on saving the credentials. %s", err)
return err
}
return nil
}
func (credentials *Credentials) Load() (bool, error) {
var err error
config := bootstrap.GetConfig()
if _, err := os.Stat(config.FactorioCredentialsFile); os.IsNotExist(err) {
return false, nil
}
fileBytes, err := ioutil.ReadFile(config.FactorioCredentialsFile)
if err != nil {
credentials.Del()
log.Printf("error reading CredentialsFile: %s", err)
return false, err
}
err = json.Unmarshal(fileBytes, credentials)
if err != nil {
credentials.Del()
log.Printf("error on unmarshal credentials_file: %s", err)
return false, err
}
if credentials.Userkey != "" && credentials.Username != "" {
return true, nil
} else {
credentials.Del()
return false, errors.New("incredients incomplete")
}
}
func (credentials *Credentials) Del() error {
var err error
config := bootstrap.GetConfig()
err = os.Remove(config.FactorioCredentialsFile)
if err != nil {
log.Printf("error delete the credentialfile: %s", err)
return err
}
return nil
}

View File

@ -1,14 +1,16 @@
package main
package factorio
import (
"log"
"github.com/hpcloud/tail"
"github.com/mroote/factorio-server-manager/bootstrap"
"log"
)
func tailLog(filename string) ([]string, error) {
func TailLog() ([]string, error) {
result := []string{}
config := bootstrap.GetConfig()
t, err := tail.TailFile(config.FactorioLog, tail.Config{Follow: false})
if err != nil {
log.Printf("Error tailing log %s", err)

View File

@ -1,4 +1,4 @@
package main
package factorio
import (
"archive/zip"
@ -26,9 +26,9 @@ type ModsResultList struct {
ModsResult []ModsResult `json:"mods"`
}
var fileLock lockfile.FileLock = lockfile.NewLock()
var FileLock lockfile.FileLock = lockfile.NewLock()
func newMods(destination string) (Mods, error) {
func NewMods(destination string) (Mods, error) {
var err error
var mods Mods
@ -47,8 +47,8 @@ func newMods(destination string) (Mods, error) {
return mods, nil
}
func (mods *Mods) listInstalledMods() ModsResultList {
var result ModsResultList
func (mods *Mods) ListInstalledMods() ModsResultList {
result := ModsResultList{make([]ModsResult, 0)}
for _, modInfo := range mods.ModInfoList.Mods {
var modsResult ModsResult
@ -73,7 +73,7 @@ func (mods *Mods) listInstalledMods() ModsResultList {
return result
}
func (mods *Mods) deleteMod(modName string) error {
func (mods *Mods) DeleteMod(modName string) error {
var err error
err = mods.ModInfoList.deleteMod(modName)
@ -95,13 +95,7 @@ func (mods *Mods) createMod(modName string, fileName string, fileRc io.Reader) e
var err error
//check if mod already exists and delete it
if mods.ModSimpleList.checkModExists(modName) {
err = mods.ModSimpleList.deleteMod(modName)
if err != nil {
log.Printf("error when deleting mod: %s", err)
return err
}
if mods.ModSimpleList.CheckModExists(modName) {
err = mods.ModInfoList.deleteMod(modName)
if err != nil {
log.Printf("error when deleting mod: %s", err)
@ -113,22 +107,33 @@ func (mods *Mods) createMod(modName string, fileName string, fileRc io.Reader) e
err = mods.ModInfoList.createMod(modName, fileName, fileRc)
if err != nil {
log.Printf("error on creating mod-file: %s", err)
// removing mod completely
err2 := mods.ModSimpleList.deleteMod(modName)
if err2 != nil {
log.Printf("error deleting mod from modSimpleList: %s", err2)
}
return err
}
err = mods.ModSimpleList.createMod(modName)
if err != nil {
log.Printf("error on adding mod to the mod-list.json: %s", err)
return err
// also add to ModSimpleList if not there yet
if !mods.ModSimpleList.CheckModExists(modName) {
err = mods.ModSimpleList.createMod(modName)
if err != nil {
log.Printf("error creating mod in modSimpleList: %s", err)
return err
}
}
return nil
}
func (mods *Mods) downloadMod(url string, filename string, modId string) error {
func (mods *Mods) DownloadMod(url string, filename string, modId string) error {
var err error
var credentials FactorioCredentials
status, err := credentials.load()
var credentials Credentials
status, err := credentials.Load()
if err != nil {
log.Printf("error loading credentials: %s", err)
return err
@ -170,21 +175,14 @@ func (mods *Mods) downloadMod(url string, filename string, modId string) error {
return nil
}
func (mods *Mods) uploadMod(header *multipart.FileHeader) error {
func (mods *Mods) UploadMod(file multipart.File, header *multipart.FileHeader) error {
var err error
if filepath.Ext(header.Filename) != ".zip" {
log.Print("The uploaded file wasn't a zip-file -> ignore it")
return nil //simply do nothing xD
log.Print("The uploaded file wasn't a zip-file")
return errors.New("the uploaded file wasn't a zip-file")
}
file, err := header.Open()
if err != nil {
log.Printf("error on open file via fileHeader. %s", err)
return err
}
defer file.Close()
fileByteArray, err := ioutil.ReadAll(file)
if err != nil {
log.Printf("error reading file: %s", err)
@ -213,10 +211,10 @@ func (mods *Mods) uploadMod(header *multipart.FileHeader) error {
return nil
}
func (mods *Mods) updateMod(modName string, url string, filename string) error {
func (mods *Mods) UpdateMod(modName string, url string, filename string) error {
var err error
err = mods.downloadMod(url, filename, modName)
err = mods.DownloadMod(url, filename, modName)
if err != nil {
log.Printf("updateMod ... error when downloading the new Mod: %s", err)
return err

View File

@ -1,4 +1,4 @@
package main
package factorio
import (
"archive/zip"
@ -50,7 +50,7 @@ func (modInfoList *ModInfoList) listInstalledMods() error {
err = filepath.Walk(modInfoList.Destination, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && filepath.Ext(path) == ".zip" {
err = fileLock.RLock(path)
err = FileLock.RLock(path)
if err != nil && err == lockfile.ErrorAlreadyLocked {
log.Println(err)
return nil
@ -58,7 +58,7 @@ func (modInfoList *ModInfoList) listInstalledMods() error {
log.Printf("error locking file: %s", err)
return err
}
defer fileLock.RUnlock(path)
defer FileLock.RUnlock(path)
zipFile, err := zip.OpenReader(path)
if err != nil {
@ -83,6 +83,7 @@ func (modInfoList *ModInfoList) listInstalledMods() error {
continue
}
// skip optional and incompatible dependencies
parts := strings.Split(dep, " ")
if len(parts) > 3 {
log.Printf("skipping dependency '%s' in '%s': invalid format\n", dep, modInfo.Name)
@ -107,11 +108,13 @@ func (modInfoList *ModInfoList) listInstalledMods() error {
break
}
server := GetFactorioServer()
if !base.Equals(NilVersion) {
modInfo.Compatibility = FactorioServ.Version.Compare(base, op)
modInfo.Compatibility = server.Version.Compare(base, op)
} else {
log.Println("error finding basemodDependency. Using FactorioVersion...")
modInfo.Compatibility = !FactorioServ.Version.Less(modInfo.FactorioVersion)
modInfo.Compatibility = !server.Version.Less(modInfo.FactorioVersion)
}
modInfoList.Mods = append(modInfoList.Mods, modInfo)
@ -136,10 +139,10 @@ func (modInfoList *ModInfoList) deleteMod(modName string) error {
if mod.Name == modName {
filePath := filepath.Join(modInfoList.Destination, mod.FileName)
fileLock.LockW(filePath)
FileLock.LockW(filePath)
//delete mod
err = os.Remove(filePath)
fileLock.Unlock(filePath)
FileLock.Unlock(filePath)
if err != nil {
log.Printf("ModInfoList ... error when deleting mod: %s", err)
return err
@ -156,8 +159,8 @@ func (modInfoList *ModInfoList) deleteMod(modName string) error {
}
}
log.Printf("the mod-file for mod %s doesn't exists!", modName)
return nil
log.Printf("the mod-file for mod %s doesn't exist!", modName)
return errors.New("the mod-file for mod " + modName + " doesn't exist!")
}
func (modInfo *ModInfo) getModInfo(reader *zip.Reader) error {
@ -207,7 +210,7 @@ func (modInfoList *ModInfoList) createMod(modName string, fileName string, modFi
}
defer newFile.Close()
fileLock.LockW(filePath)
FileLock.LockW(filePath)
_, err = io.Copy(newFile, modFile)
if err != nil {
@ -221,7 +224,7 @@ func (modInfoList *ModInfoList) createMod(modName string, fileName string, modFi
return err
}
fileLock.Unlock(filePath)
FileLock.Unlock(filePath)
//reload the list
err = modInfoList.listInstalledMods()

View File

@ -1,7 +1,8 @@
package main
package factorio
import (
"encoding/json"
"errors"
"io/ioutil"
"log"
"os"
@ -107,7 +108,7 @@ func (modSimpleList *ModSimpleList) deleteMod(modName string) error {
return nil
}
func (modSimpleList *ModSimpleList) checkModExists(modName string) bool {
func (modSimpleList *ModSimpleList) CheckModExists(modName string) bool {
for _, singleMod := range modSimpleList.Mods {
if singleMod.Name == modName {
return true
@ -138,18 +139,24 @@ func (modSimpleList *ModSimpleList) createMod(modName string) error {
return nil
}
func (modSimpleList *ModSimpleList) toggleMod(modName string) (error, bool) {
func (modSimpleList *ModSimpleList) ToggleMod(modName string) (error, bool) {
var err error
var newEnabled bool
var found bool
for index, mod := range modSimpleList.Mods {
if mod.Name == modName {
newEnabled = !modSimpleList.Mods[index].Enabled
modSimpleList.Mods[index].Enabled = newEnabled
found = true
break
}
}
if !found {
return errors.New("mod is not installed"), newEnabled
}
err = modSimpleList.saveModInfoJson()
if err != nil {
log.Printf("error on savin new ModSimpleList: %s", err)

View File

@ -1,7 +1,8 @@
package main
package factorio
import (
"errors"
"github.com/mroote/factorio-server-manager/bootstrap"
"io"
"io/ioutil"
"log"
@ -18,11 +19,8 @@ type ModPackResult struct {
Name string `json:"name"`
Mods ModsResultList `json:"mods"`
}
type ModPackResultList struct {
ModPacks []ModPackResult `json:"mod_packs"`
}
func newModPackMap() (ModPackMap, error) {
func NewModPackMap() (ModPackMap, error) {
var err error
modPackMap := make(ModPackMap)
@ -39,7 +37,7 @@ func newModPack(modPackFolder string) (*ModPack, error) {
var err error
var modPack ModPack
modPack.Mods, err = newMods(modPackFolder)
modPack.Mods, err = NewMods(modPackFolder)
if err != nil {
log.Printf("error on loading mods in mod_pack_dir: %s", err)
return &modPack, err
@ -51,6 +49,7 @@ func newModPack(modPackFolder string) (*ModPack, error) {
func (modPackMap *ModPackMap) reload() error {
var err error
newModPackMap := make(ModPackMap)
config := bootstrap.GetConfig()
err = filepath.Walk(config.FactorioModPackDir, func(path string, info os.FileInfo, err error) error {
if path == config.FactorioModPackDir || !info.IsDir() {
@ -77,26 +76,26 @@ func (modPackMap *ModPackMap) reload() error {
return nil
}
func (modPackMap *ModPackMap) listInstalledModPacks() ModPackResultList {
var modPackResultList ModPackResultList
func (modPackMap *ModPackMap) ListInstalledModPacks() []ModPackResult {
list := make([]ModPackResult, 0)
for modPackName, modPack := range *modPackMap {
var modPackResult ModPackResult
modPackResult.Name = modPackName
modPackResult.Mods = modPack.Mods.listInstalledMods()
modPackResult.Mods = modPack.Mods.ListInstalledMods()
modPackResultList.ModPacks = append(modPackResultList.ModPacks, modPackResult)
list = append(list, modPackResult)
}
return modPackResultList
return list
}
func (modPackMap *ModPackMap) createModPack(modPackName string) error {
func (modPackMap *ModPackMap) CreateModPack(modPackName string) error {
var err error
config := bootstrap.GetConfig()
modPackFolder := filepath.Join(config.FactorioModPackDir, modPackName)
if modPackMap.checkModPackExists(modPackName) == true {
if modPackMap.CheckModPackExists(modPackName) == true {
log.Printf("ModPack %s already existis", modPackName)
return errors.New("ModPack " + modPackName + " already exists, please choose a different name")
}
@ -116,7 +115,7 @@ func (modPackMap *ModPackMap) createModPack(modPackName string) error {
files, err := ioutil.ReadDir(config.FactorioModsDir)
if err != nil {
log.Printf("error on reading the dactorio mods dir: %s", err)
log.Printf("error on reading the factorio mods dir: %s", err)
return err
}
@ -153,14 +152,39 @@ func (modPackMap *ModPackMap) createModPack(modPackName string) error {
//reload the ModPackList
err = modPackMap.reload()
if err != nil {
log.Printf("error on reloading ModPack: %s", err)
log.Printf("error reloading ModPack: %s", err)
return err
}
return nil
}
func (modPackMap *ModPackMap) checkModPackExists(modPackName string) bool {
func (modPackMap *ModPackMap) CreateEmptyModPack(packName string) error {
var err error
config := bootstrap.GetConfig()
modPackFolder := filepath.Join(config.FactorioModPackDir, packName)
if modPackMap.CheckModPackExists(packName) == true {
log.Printf("ModPack %s already existis", packName)
return errors.New("ModPack " + packName + " already exists, please choose a different name")
}
// Create the modPack-folder
err = os.MkdirAll(modPackFolder, 0777)
if err != nil {
log.Printf("error creating the new ModPack directory: %s", err)
return err
}
err = modPackMap.reload()
if err != nil {
log.Printf("error reloading ModPack: %s", err)
return err
}
return nil
}
func (modPackMap *ModPackMap) CheckModPackExists(modPackName string) bool {
for modPackId := range *modPackMap {
if modPackId == modPackName {
return true
@ -170,9 +194,9 @@ func (modPackMap *ModPackMap) checkModPackExists(modPackName string) bool {
return false
}
func (modPackMap *ModPackMap) deleteModPack(modPackName string) error {
func (modPackMap *ModPackMap) DeleteModPack(modPackName string) error {
var err error
config := bootstrap.GetConfig()
modPackDir := filepath.Join(config.FactorioModPackDir, modPackName)
err = os.RemoveAll(modPackDir)
@ -190,9 +214,9 @@ func (modPackMap *ModPackMap) deleteModPack(modPackName string) error {
return nil
}
func (modPack *ModPack) loadModPack() error {
func (modPack *ModPack) LoadModPack() error {
var err error
config := bootstrap.GetConfig()
//get filemode, so it can be restored
fileInfo, err := os.Stat(config.FactorioModsDir)
if err != nil {

149
src/factorio/mod_portal.go Normal file
View File

@ -0,0 +1,149 @@
package factorio
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/url"
"time"
)
type ModPortalStruct struct {
DownloadsCount int `json:"downloads_count"`
Name string `json:"name"`
Owner string `json:"owner"`
Releases []struct {
DownloadURL string `json:"download_url"`
FileName string `json:"file_name"`
InfoJSON struct {
FactorioVersion Version `json:"factorio_version"`
} `json:"info_json"`
ReleasedAt time.Time `json:"released_at"`
Sha1 string `json:"sha1"`
Version Version `json:"version"`
Compatibility bool `json:"compatibility"`
} `json:"releases"`
Summary string `json:"summary"`
Title string `json:"title"`
}
// get all mods uploaded to the factorio modPortal
func ModPortalList() (interface{}, error, int) {
req, err := http.NewRequest(http.MethodGet, "https://mods.factorio.com/api/mods?page_size=max", nil)
if err != nil {
return "error", err, http.StatusInternalServerError
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "error", err, http.StatusInternalServerError
}
defer resp.Body.Close()
text, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "error", err, http.StatusInternalServerError
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New(string(text)), resp.StatusCode
}
var jsonVal interface{}
err = json.Unmarshal(text, &jsonVal)
if err != nil {
return "error", err, http.StatusInternalServerError
}
return jsonVal, nil, resp.StatusCode
}
// get the details (mod-info, releases, etc.) from a specific mod from the modPortal
func ModPortalModDetails(modId string) (ModPortalStruct, error, int) {
var mod ModPortalStruct
req, err := http.NewRequest(http.MethodGet, "https://mods.factorio.com/api/mods/"+modId, nil)
if err != nil {
return mod, err, http.StatusInternalServerError
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return mod, err, http.StatusInternalServerError
}
defer resp.Body.Close()
text, err := ioutil.ReadAll(resp.Body)
if err != nil {
return mod, err, http.StatusInternalServerError
}
err = json.Unmarshal(text, &mod)
if err != nil {
return mod, err, http.StatusInternalServerError
}
if resp.StatusCode != http.StatusOK {
return ModPortalStruct{}, errors.New(string(text)), resp.StatusCode
}
server := GetFactorioServer()
installedBaseVersion := Version{}
_ = installedBaseVersion.UnmarshalText([]byte(server.BaseModVersion))
requiredVersion := NilVersion
for key, release := range mod.Releases {
requiredVersion = release.InfoJSON.FactorioVersion
areVersionIdentical := requiredVersion.Equals(installedBaseVersion)
isException := installedBaseVersion.Equals(Version{1, 0, 0, 0}) && requiredVersion.Equals(Version{0, 18, 0, 0})
release.Compatibility = areVersionIdentical || isException
mod.Releases[key] = release
}
return mod, nil, resp.StatusCode
}
//Log the user into factorio, so mods can be downloaded
func FactorioLogin(username string, password string) (error, int) {
var err error
resp, err := http.PostForm("https://auth.factorio.com/api-login",
url.Values{"require_game_ownership": {"true"}, "username": {username}, "password": {password}})
if err != nil {
return err, http.StatusInternalServerError
}
defer resp.Body.Close()
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err, http.StatusInternalServerError
}
bodyString := string(bodyBytes)
if resp.StatusCode != http.StatusOK {
return errors.New(bodyString), resp.StatusCode
}
var successResponse []string
err = json.Unmarshal(bodyBytes, &successResponse)
if err != nil {
return err, http.StatusInternalServerError
}
credentials := Credentials{
Username: username,
Userkey: successResponse[0],
}
err = credentials.Save()
if err != nil {
return err, http.StatusInternalServerError
}
return nil, http.StatusOK
}

View File

@ -1,14 +1,13 @@
package main
package factorio
import (
"archive/zip"
"bytes"
"encoding/json"
"errors"
"github.com/mroote/factorio-server-manager/bootstrap"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
@ -21,171 +20,10 @@ type LoginErrorResponse struct {
type LoginSuccessResponse struct {
UserKey []string `json:""`
}
type FactorioCredentials struct {
Username string `json:"username"`
Userkey string `json:"userkey"`
}
func (credentials *FactorioCredentials) save() error {
func DeleteAllMods() error {
var err error
credentialsJson, err := json.Marshal(credentials)
if err != nil {
log.Printf("error mashalling the credentials: %s", err)
return err
}
err = ioutil.WriteFile(config.FactorioCredentialsFile, credentialsJson, 0664)
if err != nil {
log.Printf("error on saving the credentials. %s", err)
return err
}
return nil
}
func (credentials *FactorioCredentials) load() (bool, error) {
var err error
if _, err := os.Stat(config.FactorioCredentialsFile); os.IsNotExist(err) {
return false, nil
}
fileBytes, err := ioutil.ReadFile(config.FactorioCredentialsFile)
if err != nil {
credentials.del()
log.Printf("error reading CredentialsFile: %s", err)
return false, err
}
err = json.Unmarshal(fileBytes, credentials)
if err != nil {
credentials.del()
log.Printf("error on unmarshal credentials_file: %s", err)
return false, err
}
if credentials.Userkey != "" && credentials.Username != "" {
return true, nil
} else {
credentials.del()
return false, errors.New("incredients incomplete")
}
}
func (credentials *FactorioCredentials) del() error {
var err error
err = os.Remove(config.FactorioCredentialsFile)
if err != nil {
log.Printf("error delete the credentialfile: %s", err)
return err
}
return nil
}
//Log the user into factorio, so mods can be downloaded
func factorioLogin(username string, password string) (string, error, int) {
var err error
resp, err := http.PostForm("https://auth.factorio.com/api-login",
url.Values{"require_game_ownership": {"true"}, "username": {username}, "password": {password}})
if err != nil {
log.Printf("error on logging in: %s", err)
return "", err, resp.StatusCode
}
defer resp.Body.Close()
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("error on reading resp.Body: %s", err)
return "", err, http.StatusInternalServerError
}
bodyString := string(bodyBytes)
if resp.StatusCode != http.StatusOK {
log.Println("error Statuscode not 200")
return bodyString, errors.New(bodyString), resp.StatusCode
}
var successResponse []string
err = json.Unmarshal(bodyBytes, &successResponse)
if err != nil {
log.Printf("error on unmarshal body: %s", err)
return err.Error(), err, http.StatusInternalServerError
}
credentials := FactorioCredentials{
Username: username,
Userkey: successResponse[0],
}
err = credentials.save()
if err != nil {
log.Printf("error saving the credentials. %s", err)
return err.Error(), err, http.StatusInternalServerError
}
return "", nil, http.StatusOK
}
//Search inside the factorio mod portal
func searchModPortal(keyword string) (string, error, int) {
req, err := http.NewRequest(http.MethodGet, "https://mods.factorio.com/api/mods", nil)
if err != nil {
return "error", err, 500
}
query := req.URL.Query()
query.Add("q", keyword)
req.URL.RawQuery = query.Encode()
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "error", err, 500
}
text, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return "error", err, 500
}
textString := string(text)
return textString, nil, resp.StatusCode
}
func getModDetails(modId string) (string, error, int) {
var err error
newLink := "https://mods.factorio.com/api/mods/" + modId
resp, err := http.Get(newLink)
if err != nil {
return "error", err, http.StatusInternalServerError
}
//get the response-text
text, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
textString := string(text)
if err != nil {
log.Fatal(err)
return "error", err, resp.StatusCode
}
return textString, nil, resp.StatusCode
}
func deleteAllMods() error {
var err error
config := bootstrap.GetConfig()
modsDirInfo, err := os.Stat(config.FactorioModsDir)
if err != nil {
log.Printf("error getting stats of FactorioModsDir: %s", err)
@ -209,13 +47,12 @@ func deleteAllMods() error {
return nil
}
func modStartUp() {
var err error
func ModStartUp() {
config := bootstrap.GetConfig()
//get main-folder info
factorioDirInfo, err := os.Stat(config.FactorioDir)
if err != nil {
log.Printf("error getting stats from FactorioDir: %s", err)
log.Printf("error getting stats from FactorioDir %s with error %s", config.FactorioDir, err)
return
}
factorioDirPerm := factorioDirInfo.Mode().Perm()
@ -229,7 +66,7 @@ func modStartUp() {
//crate mod_pack dir
if _, err = os.Stat(config.FactorioModPackDir); os.IsNotExist(err) {
log.Println("no ModPackDir found ... creating one ...")
os.Mkdir(config.FactorioModPackDir, factorioDirPerm)
_ = os.Mkdir(config.FactorioModPackDir, factorioDirPerm)
}
oldModpackDir := filepath.Join(config.FactorioDir, "modpacks")
@ -276,7 +113,7 @@ func modStartUp() {
}
newJson, _ := json.Marshal(modSimpleList)
err = ioutil.WriteFile(filepath.Join(modSimpleList.Destination,"mod-list.json"), newJson, 0664)
err = ioutil.WriteFile(filepath.Join(modSimpleList.Destination, "mod-list.json"), newJson, 0664)
if err != nil {
log.Printf("error when writing new mod-list: %s", err)
return err
@ -288,7 +125,7 @@ func modStartUp() {
}
defer modPackFile.Close()
mods, err := newMods(modPackDir)
mods, err := NewMods(modPackDir)
if err != nil {
log.Printf("error reading mods: %s", err)
return err

View File

@ -1,6 +1,7 @@
package main
package factorio
import (
"github.com/mroote/factorio-server-manager/bootstrap"
"log"
"strconv"
@ -9,8 +10,10 @@ import (
func connectRC() error {
var err error
config := bootstrap.GetConfig()
rconAddr := config.ServerIP + ":" + strconv.Itoa(config.FactorioRconPort)
FactorioServ.Rcon, err = rcon.Dial(rconAddr, config.FactorioRconPass)
server := GetFactorioServer()
server.Rcon, err = rcon.Dial(rconAddr, config.FactorioRconPass)
if err != nil {
log.Printf("Cannot create rcon session: %s", err)
return err

View File

@ -1,4 +1,4 @@
package main
package factorio
import (
"archive/zip"

326
src/factorio/save_test.go Normal file
View File

@ -0,0 +1,326 @@
package factorio
import (
"testing"
)
// 0.18 Binary seems equal to 0.17 binary, just the default values changed
func Test0_18(t *testing.T) {
file, err := OpenArchiveFile("../factorio_testfiles/test_0_18.zip", "level.dat")
if err != nil {
t.Fatalf("Error opening level.dat: %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{0, 18, 2, 2},
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{0, 18, 2},
LoadedFromBuild: 49204,
AllowedCommands: 1,
Mods: []Mod{
{
Version: Version{0, 18, 2},
Name: "base",
},
{
Version: Version{2, 0, 0},
Name: "belt-balancer",
},
{
Version: Version{2, 0, 1},
Name: "train-station-overview",
},
},
}
header.Equals(testHeader, t)
}
func Test0_17(t *testing.T) {
file, err := OpenArchiveFile("../factorio_testfiles/test_0_17.zip", "level.dat")
if err != nil {
t.Fatalf("Error opening level.dat: %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{0, 17, 1, 1},
Campaign: "transport-belt-madness",
Name: "level-01",
BaseMod: "base",
Difficulty: 0,
Finished: false,
PlayerWon: false,
NextLevel: "",
CanContinue: false,
FinishedButContinuing: false,
SavingReplay: true,
AllowNonAdminDebugOptions: true,
LoadedFrom: Version{0, 17, 1},
LoadedFromBuild: 43001,
AllowedCommands: 1,
Mods: []Mod{
{
Version: Version{0, 2, 0},
Name: "Warehousing",
},
{
Version: Version{0, 17, 1},
Name: "base",
},
},
}
header.Equals(testHeader, t)
}
func Test0_16(t *testing.T) {
file, err := OpenArchiveFile("../factorio_testfiles/test_0_16.zip", "level.dat")
if err != nil {
t.Fatalf("Error opening level.dat: %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{0, 16, 51, 0},
Campaign: "transport-belt-madness",
Name: "level-01",
BaseMod: "base",
Difficulty: 0,
Finished: false,
PlayerWon: false,
NextLevel: "",
CanContinue: false,
FinishedButContinuing: false,
SavingReplay: true,
AllowNonAdminDebugOptions: true,
LoadedFrom: Version{0, 16, 51},
LoadedFromBuild: 36654,
AllowedCommands: 1,
Mods: []Mod{
{
Version: Version{0, 1, 3},
Name: "Warehousing",
},
{
Version: Version{0, 16, 51},
Name: "base",
},
},
}
header.Equals(testHeader, t)
}
func Test0_15(t *testing.T) {
file, err := OpenArchiveFile("../factorio_testfiles/test_0_15.zip", "level.dat")
if err != nil {
t.Fatalf("Error opening level.dat: %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{0, 15, 40, 0},
Campaign: "transport-belt-madness",
Name: "level-01",
BaseMod: "base",
Difficulty: 0,
Finished: false,
PlayerWon: false,
NextLevel: "",
CanContinue: false,
FinishedButContinuing: false,
SavingReplay: true,
LoadedFrom: Version{0, 15, 40},
LoadedFromBuild: 30950,
AllowedCommands: 1,
Mods: []Mod{
{
Version: Version{0, 0, 13},
Name: "Warehousing",
},
{
Version: Version{0, 15, 40},
Name: "base",
},
},
}
header.Equals(testHeader, t)
}
func Test0_14(t *testing.T) {
file, err := OpenArchiveFile("../factorio_testfiles/test_0_14.zip", "level.dat")
if err != nil {
t.Fatalf("Error opening level.dat: %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{0, 14, 23, 0},
Campaign: "transport-belt-madness",
Name: "level-01",
BaseMod: "base",
Difficulty: 1,
Finished: false,
PlayerWon: false,
NextLevel: "",
CanContinue: false,
FinishedButContinuing: false,
SavingReplay: true,
LoadedFrom: Version{0, 14, 23},
LoadedFromBuild: 25374,
AllowedCommands: 1,
Mods: []Mod{
{
Version: Version{0, 0, 11},
Name: "Warehousing",
},
{
Version: Version{0, 14, 23},
Name: "base",
},
},
}
header.Equals(testHeader, t)
}
func Test0_13(t *testing.T) {
file, err := OpenArchiveFile("../factorio_testfiles/test_0_13.zip", "level.dat")
if err != nil {
t.Fatalf("Error opening level.dat: %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{0, 13, 20, 0},
Campaign: "transport-belt-madness",
Name: "level-01",
BaseMod: "base",
Difficulty: 1,
Finished: false,
PlayerWon: false,
NextLevel: "",
CanContinue: false,
FinishedButContinuing: false,
SavingReplay: true,
LoadedFrom: Version{0, 13, 20},
LoadedFromBuild: 24011,
AllowedCommands: 1,
Mods: []Mod{
{
Version: Version{1, 1, 0},
Name: "Extra-Virtual-Signals",
},
{
Version: Version{0, 13, 20},
Name: "base",
},
},
}
header.Equals(testHeader, t)
}
func (h *SaveHeader) Equals(other SaveHeader, t *testing.T) {
if h.FactorioVersion != other.FactorioVersion {
t.Errorf("FactorioVersion not equal: %s --- %s", h.FactorioVersion, other.FactorioVersion)
}
if h.Campaign != other.Campaign {
t.Errorf("Campaign not equal: %s --- %s", h.Campaign, other.Campaign)
}
if h.Name != other.Name {
t.Errorf("Name not equal: %s --- %s", h.Name, other.Name)
}
if h.BaseMod != other.BaseMod {
t.Errorf("BaseMod not equal: %s --- %s", h.BaseMod, other.BaseMod)
}
if h.Difficulty != other.Difficulty {
t.Errorf("Difficulty not equal: %d --- %d", h.Difficulty, other.Difficulty)
}
if h.Finished != other.Finished {
t.Errorf("Finished not equal: %t --- %t", h.Finished, other.Finished)
}
if h.PlayerWon != other.PlayerWon {
t.Errorf("PlayerWon not equal: %t --- %t", h.PlayerWon, other.PlayerWon)
}
if h.NextLevel != other.NextLevel {
t.Errorf("NextLevel not equal: %s --- %s", h.NextLevel, other.NextLevel)
}
if h.CanContinue != other.CanContinue {
t.Errorf("CanContinue not equal: %t --- %t", h.CanContinue, other.CanContinue)
}
if h.FinishedButContinuing != other.FinishedButContinuing {
t.Errorf("FinishedButContinuing not equal: %t --- %t", h.FinishedButContinuing, other.FinishedButContinuing)
}
if h.SavingReplay != other.SavingReplay {
t.Errorf("SavingReplay not equal: %t --- %t", h.SavingReplay, other.SavingReplay)
}
if h.AllowNonAdminDebugOptions != other.AllowNonAdminDebugOptions {
t.Errorf("AllowNonAdminDebugOptions not equal: %t --- %t", h.AllowNonAdminDebugOptions, other.AllowNonAdminDebugOptions)
}
if h.LoadedFrom != other.LoadedFrom {
t.Errorf("LoadedFrom not equal: %s --- %s", h.LoadedFrom, other.LoadedFrom)
}
if h.LoadedFromBuild != other.LoadedFromBuild {
t.Errorf("LoadedFromBuild not equal: %d --- %d", h.LoadedFromBuild, other.LoadedFromBuild)
}
if h.AllowedCommands != other.AllowedCommands {
t.Errorf("AllowedCommands not equal: %d --- %d", h.AllowedCommands, other.AllowedCommands)
}
for k := range h.Mods {
if h.Mods[k].Name != other.Mods[k].Name {
t.Errorf("ModNames not equal: %s --- %s", h.Mods[k].Name, other.Mods[k].Name)
} else if h.Mods[k].Version != other.Mods[k].Version {
t.Errorf("ModVersions of Mod %s are not equal: %s --- %s", h.Mods[k].Name, h.Mods[k].Version, other.Mods[k].Version)
}
}
}

View File

@ -1,8 +1,9 @@
package main
package factorio
import (
"errors"
"fmt"
"github.com/mroote/factorio-server-manager/bootstrap"
"log"
"os"
"os/exec"
@ -21,7 +22,7 @@ func (s Save) String() string {
}
// Lists save files in factorio/saves
func listSaves(saveDir string) (saves []Save, err error) {
func ListSaves(saveDir string) (saves []Save, err error) {
saves = []Save{}
err = filepath.Walk(saveDir, func(path string, info os.FileInfo, err error) error {
if info.IsDir() && info.Name() == "saves" {
@ -37,8 +38,9 @@ func listSaves(saveDir string) (saves []Save, err error) {
return
}
func findSave(name string) (*Save, error) {
saves, err := listSaves(config.FactorioSavesDir)
func FindSave(name string) (*Save, error) {
config := bootstrap.GetConfig()
saves, err := ListSaves(config.FactorioSavesDir)
if err != nil {
return nil, fmt.Errorf("error listing saves: %v", err)
}
@ -52,23 +54,24 @@ func findSave(name string) (*Save, error) {
return nil, errors.New("save not found")
}
func (s *Save) remove() error {
func (s *Save) Remove() error {
if s.Name == "" {
return errors.New("save name cannot be blank")
}
config := bootstrap.GetConfig()
return os.Remove(filepath.Join(config.FactorioSavesDir, s.Name))
}
// Create savefiles for Factorio
func createSave(filePath string) (string, error) {
err := os.MkdirAll(filepath.Base(filePath), 0755)
func CreateSave(filePath string) (string, error) {
err := os.MkdirAll(filepath.Dir(filePath), 0755)
if err != nil {
log.Printf("Error in creating Factorio save: %s", err)
return "", err
}
args := []string{"--create", filePath}
config := bootstrap.GetConfig()
cmdOutput, err := exec.Command(config.FactorioBinary, args...).Output()
if err != nil {
log.Printf("Error in creating Factorio save: %s", err)

View File

@ -1,33 +1,31 @@
package main
package factorio
import (
"bufio"
"encoding/json"
"fmt"
"github.com/mroote/factorio-server-manager/api/websocket"
"github.com/mroote/factorio-server-manager/bootstrap"
"io"
"io/ioutil"
"log"
"math/rand"
"os"
"os/exec"
"path/filepath"
"runtime"
"regexp"
"strconv"
"strings"
"time"
"regexp"
"sync"
"github.com/majormjr/rcon"
)
type FactorioServer struct {
type Server struct {
Cmd *exec.Cmd `json:"-"`
Savefile string `json:"savefile"`
Latency int `json:"latency"`
BindIP string `json:"bindip"`
Port int `json:"port"`
Running bool `json:"running"`
running bool `json:"running"`
Version Version `json:"fac_version"`
BaseModVersion string `json:"base_mod_version"`
StdOut io.ReadCloser `json:"-"`
@ -38,73 +36,117 @@ type FactorioServer struct {
LogChan chan []string `json:"-"`
}
func randomPort() int {
// Returns random port to use for rcon connection
return rand.Intn(45000-40000) + 40000
var instantiated Server
var once sync.Once
func (server *Server) SetRunning(newState bool) {
if server.running != newState {
log.Println("new state, will also send to correct room")
server.running = newState
wsRoom := websocket.WebsocketHub.GetRoom("server_status")
wsRoom.Send("Server status has changed")
}
}
func initFactorio() (f *FactorioServer, err error) {
f = new(FactorioServer)
f.Settings = make(map[string]interface{})
func (server *Server) GetRunning() bool {
return server.running
}
func (server *Server) autostart() {
var err error
if server.BindIP == "" {
server.BindIP = "0.0.0.0"
}
if server.Port == 0 {
server.Port = 34197
}
server.Savefile = "Load Latest"
err = server.Run()
if err != nil {
log.Printf("Error starting Factorio server: %+v", err)
return
}
}
func SetFactorioServer(server Server) {
instantiated = server
}
func NewFactorioServer() (err error) {
server := Server{}
server.Settings = make(map[string]interface{})
config := bootstrap.GetConfig()
if err = os.MkdirAll(config.FactorioConfigDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create config directory: %v", err)
log.Printf("failed to create config directory: %v", err)
return
}
settingsPath := filepath.Join(config.FactorioConfigDir, config.SettingsFile)
var settings *os.File
if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
if _, err = os.Stat(settingsPath); os.IsNotExist(err) {
// copy example settings to supplied settings file, if not exists
log.Printf("Server settings at %s not found, copying example server settings.\n", settingsPath)
examplePath := filepath.Join(config.FactorioDir, "data", "server-settings.example.json")
example, err := os.Open(examplePath)
var example *os.File
example, err = os.Open(examplePath)
if err != nil {
return nil, fmt.Errorf("failed to open example server settings: %v", err)
log.Printf("failed to open example server settings: %v", err)
return
}
defer example.Close()
settings, err = os.Create(settingsPath)
if err != nil {
return nil, fmt.Errorf("failed to create server settings file: %v", err)
log.Printf("failed to create server settings file: %v", err)
return
}
defer settings.Close()
_, err = io.Copy(settings, example)
if err != nil {
return nil, fmt.Errorf("failed to copy example server settings: %v", err)
log.Printf("failed to copy example server settings: %v", err)
return
}
err = example.Close()
if err != nil {
return nil, fmt.Errorf("failed to close example server settings: %s", err)
log.Printf("failed to close example server settings: %s", err)
return
}
} else {
// otherwise, open file normally
settings, err = os.Open(settingsPath)
if err != nil {
return nil, fmt.Errorf("failed to open server settings file: %v", err)
log.Printf("failed to open server settings file: %v", err)
return
}
defer settings.Close()
}
// before reading reset offset
if _, err = settings.Seek(0, 0); err != nil {
return nil, fmt.Errorf("error while seeking in settings file: %v", err)
log.Printf("error while seeking in settings file: %v", err)
return
}
if err = json.NewDecoder(settings).Decode(&f.Settings); err != nil {
return nil, fmt.Errorf("error reading %s: %v", settingsPath, err)
if err = json.NewDecoder(settings).Decode(&server.Settings); err != nil {
log.Printf("error reading %s: %v", settingsPath, err)
return
}
log.Printf("Loaded Factorio settings from %s\n", settingsPath)
out := []byte{}
//Load factorio version
if config.glibcCustom == "true" {
out, err = exec.Command(config.glibcLocation, "--library-path", config.glibcLibLoc, config.FactorioBinary, "--version").Output()
if config.GlibcCustom == "true" {
out, err = exec.Command(config.GlibcLocation, "--library-path", config.GlibcLibLoc, config.FactorioBinary, "--version").Output()
} else {
out, err = exec.Command(config.FactorioBinary, "--version").Output()
}
@ -116,7 +158,7 @@ func initFactorio() (f *FactorioServer, err error) {
reg := regexp.MustCompile("Version.*?((\\d+\\.)?(\\d+\\.)?(\\*|\\d+)+)")
found := reg.FindStringSubmatch(string(out))
err = f.Version.UnmarshalText([]byte(found[1]))
err = server.Version.UnmarshalText([]byte(found[1]))
if err != nil {
log.Printf("could not parse version: %v", err)
return
@ -136,38 +178,50 @@ func initFactorio() (f *FactorioServer, err error) {
return
}
f.BaseModVersion = modInfo.Version
server.BaseModVersion = modInfo.Version
// load admins from additional file
if(f.Version.Greater(Version{0,17,0})) {
if _, err := os.Stat(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile)); os.IsNotExist(err) {
if (server.Version.Greater(Version{0, 17, 0})) {
if _, err = os.Stat(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile)); os.IsNotExist(err) {
//save empty admins-file
ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile), []byte("[]"), 0664)
_ = ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile), []byte("[]"), 0664)
} else {
data, err := ioutil.ReadFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile))
var data []byte
data, err = ioutil.ReadFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile))
if err != nil {
log.Printf("Error loading FactorioAdminFile: %s", err)
return f, err
return
}
var jsonData interface{}
err = json.Unmarshal(data, &jsonData)
if err != nil {
log.Printf("Error unmarshalling FactorioAdminFile: %s", err)
return f, err
return
}
f.Settings["admins"] = jsonData
server.Settings["admins"] = jsonData
}
}
SetFactorioServer(server)
// autostart factorio is configured to do so
if config.Autostart == "true" {
go instantiated.autostart()
}
return
}
func (f *FactorioServer) Run() error {
var err error
func GetFactorioServer() (f *Server) {
return &instantiated
}
data, err := json.MarshalIndent(f.Settings, "", " ")
func (server *Server) Run() error {
var err error
config := bootstrap.GetConfig()
data, err := json.MarshalIndent(server.Settings, "", " ")
if err != nil {
log.Println("Failed to marshal FactorioServerSettings: ", err)
} else {
@ -178,98 +232,104 @@ func (f *FactorioServer) Run() error {
//The factorio server refenences its executable-path, since we execute the ld.so file and pass the factorio binary as a parameter
//the game would use the path to the ld.so file as it's executable path and crash, to prevent this the parameter "--executable-path" is added
if config.glibcCustom == "true" {
log.Println("Custom glibc selected, glibc.so location:", config.glibcLocation, " lib location:", config.glibcLibLoc)
args = append(args, "--library-path", config.glibcLibLoc, config.FactorioBinary, "--executable-path", config.FactorioBinary)
if config.GlibcCustom == "true" {
log.Println("Custom glibc selected, glibc.so location:", config.GlibcLocation, " lib location:", config.GlibcLibLoc)
args = append(args, "--library-path", config.GlibcLibLoc, config.FactorioBinary, "--executable-path", config.FactorioBinary)
}
args = append(args,
"--bind", (f.BindIP),
"--port", strconv.Itoa(f.Port),
"--bind", server.BindIP,
"--port", strconv.Itoa(server.Port),
"--server-settings", filepath.Join(config.FactorioConfigDir, config.SettingsFile),
"--rcon-port", strconv.Itoa(config.FactorioRconPort),
"--rcon-password", config.FactorioRconPass)
if(f.Version.Greater(Version{0,17,0})) {
if (server.Version.Greater(Version{0, 17, 0})) {
args = append(args, "--server-adminlist", filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile))
}
if f.Savefile == "Load Latest" {
if server.Savefile == "Load Latest" {
args = append(args, "--start-server-load-latest")
} else {
args = append(args, "--start-server", filepath.Join(config.FactorioSavesDir, f.Savefile))
args = append(args, "--start-server", filepath.Join(config.FactorioSavesDir, server.Savefile))
}
if config.glibcCustom == "true" {
log.Println("Starting server with command: ", config.glibcLocation, args)
f.Cmd = exec.Command(config.glibcLocation, args...)
if config.GlibcCustom == "true" {
log.Println("Starting server with command: ", config.GlibcLocation, args)
server.Cmd = exec.Command(config.GlibcLocation, args...)
} else {
log.Println("Starting server with command: ", config.FactorioBinary, args)
f.Cmd = exec.Command(config.FactorioBinary, args...)
server.Cmd = exec.Command(config.FactorioBinary, args...)
}
f.StdOut, err = f.Cmd.StdoutPipe()
server.StdOut, err = server.Cmd.StdoutPipe()
if err != nil {
log.Printf("Error opening stdout pipe: %s", err)
return err
}
f.StdIn, err = f.Cmd.StdinPipe()
server.StdIn, err = server.Cmd.StdinPipe()
if err != nil {
log.Printf("Error opening stdin pipe: %s", err)
return err
}
f.StdErr, err = f.Cmd.StderrPipe()
server.StdErr, err = server.Cmd.StderrPipe()
if err != nil {
log.Printf("Error opening stderr pipe: %s", err)
return err
}
go f.parseRunningCommand(f.StdOut)
go f.parseRunningCommand(f.StdErr)
go server.parseRunningCommand(server.StdOut)
go server.parseRunningCommand(server.StdErr)
err = f.Cmd.Start()
err = server.Cmd.Start()
if err != nil {
log.Printf("Factorio process failed to start: %s", err)
return err
}
f.Running = true
server.SetRunning(true)
err = f.Cmd.Wait()
err = server.Cmd.Wait()
if err != nil {
log.Printf("Factorio process exited with error: %s", err)
f.Running = false
server.SetRunning(false)
return err
}
return nil
}
func (f *FactorioServer) parseRunningCommand(std io.ReadCloser) (err error) {
func (server *Server) parseRunningCommand(std io.ReadCloser) (err error) {
stdScanner := bufio.NewScanner(std)
for stdScanner.Scan() {
log.Printf("Factorio Server: %s", stdScanner.Text())
if err := f.writeLog(stdScanner.Text()); err != nil {
text := stdScanner.Text()
log.Printf("Factorio Server: %s", text)
if err := server.writeLog(text); err != nil {
log.Printf("Error: %s", err)
}
line := strings.Fields(stdScanner.Text())
// send the reported line per websocket
wsRoom := websocket.WebsocketHub.GetRoom("gamelog")
go wsRoom.Send(text)
line := strings.Fields(text)
// Ensure logline slice is in bounds
if len(line) > 1 {
// Check if Factorio Server reports any errors if so handle it
if line[1] == "Error" {
err := f.checkLogError(line)
err := server.checkLogError(line)
if err != nil {
log.Printf("Error checking Factorio Server Error: %s", err)
}
}
// If rcon port opens indicated in log connect to rcon
rconLog := "Starting RCON interface at port " + strconv.Itoa(config.FactorioRconPort)
rconLog := "Starting RCON interface at IP"
// check if slice index is greater than 2 to prevent panic
if len(line) > 2 {
// log line for opened rcon connection
if strings.Join(line[3:], " ") == rconLog {
if strings.Contains(strings.Join(line, " "), rconLog) {
log.Printf("Rcon running on Factorio Server")
err = connectRC()
if err != nil {
@ -286,7 +346,8 @@ func (f *FactorioServer) parseRunningCommand(std io.ReadCloser) (err error) {
return nil
}
func (f *FactorioServer) writeLog(logline string) error {
func (server *Server) writeLog(logline string) error {
config := bootstrap.GetConfig()
logfileName := filepath.Join(config.FactorioDir, "factorio-server-console.log")
file, err := os.OpenFile(logfileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
@ -305,92 +366,33 @@ func (f *FactorioServer) writeLog(logline string) error {
return nil
}
func (f *FactorioServer) checkLogError(logline []string) error {
func (server *Server) checkLogError(logline []string) error {
// TODO Handle errors generated by running Factorio Server
log.Println(logline)
return nil
}
func (f *FactorioServer) Stop() error {
if runtime.GOOS == "windows" {
// Disable our own handling of CTRL+C, so we don't close when we send it to the console.
setCtrlHandlingIsDisabledForThisProcess(true)
// Send CTRL+C to all processes attached to the console (ourself, and the factorio server instance)
sendCtrlCToPid(0)
log.Println("Sent SIGINT to Factorio process. Factorio shutting down...")
// Somehow, the Factorio devs managed to code the game to react appropriately to CTRL+C, including
// saving the game, but not actually exit. So, we still have to manually kill the process, and
// for extra fun, there's no way to know when the server save has actually completed (unless we want
// to inject filesystem logic into what should be a process-level Stop() routine), so our best option
// is to just wait an arbitrary amount of time and hope that the save is successful in that time.
time.Sleep(2 * time.Second)
f.Cmd.Process.Signal(os.Kill)
// Re-enable handling of CTRL+C after we're sure that the factrio server is shut down.
setCtrlHandlingIsDisabledForThisProcess(false)
f.Running = false
return nil
}
err := f.Cmd.Process.Signal(os.Interrupt)
if err != nil {
if err.Error() == "os: process already finished" {
f.Running = false
return err
}
log.Printf("Error sending SIGINT to Factorio process: %s", err)
return err
}
f.Running = false
log.Printf("Sent SIGINT to Factorio process. Factorio shutting down...")
err = f.Rcon.Close()
if err != nil {
log.Printf("Error close rcon connection: %s", err)
}
return nil
func init() {
websocket.WebsocketHub.RegisterControlHandler <- serverWebsocketControl
}
func (f *FactorioServer) Kill() error {
if runtime.GOOS == "windows" {
// react to websocket control messages and run the command if it is requested
func serverWebsocketControl(controls websocket.WsControls) {
log.Println(controls)
if controls.Type == "command" {
command := controls.Value
server := GetFactorioServer()
if server.GetRunning() {
log.Printf("Received command: %v", command)
err := f.Cmd.Process.Signal(os.Kill)
if err != nil {
if err.Error() == "os: process already finished" {
f.Running = false
return err
reqId, err := server.Rcon.Write(command)
if err != nil {
log.Printf("Error sending rcon command: %s", err)
return
}
log.Printf("Error sending SIGKILL to Factorio process: %s", err)
return err
log.Printf("Command send to Factorio: %s, with rcon request id: %v", command, reqId)
}
f.Running = false
log.Println("Sent SIGKILL to Factorio process. Factorio forced to exit.")
return nil
}
err := f.Cmd.Process.Signal(os.Kill)
if err != nil {
if err.Error() == "os: process already finished" {
f.Running = false
return err
}
log.Printf("Error sending SIGKILL to Factorio process: %s", err)
return err
}
f.Running = false
log.Printf("Sent SIGKILL to Factorio process. Factorio forced to exit.")
err = f.Rcon.Close()
if err != nil {
log.Printf("Error close rcon connection: %s", err)
}
return nil
}

View File

@ -0,0 +1,53 @@
// use this file only when compiling not windows (all unix systems)
// +build !windows
package factorio
import (
"log"
"os"
)
// Stubs for windows-only functions
func (server *Server) Kill() error {
err := server.Cmd.Process.Signal(os.Kill)
if err != nil {
if err.Error() == "os: process already finished" {
server.SetRunning(false)
return err
}
log.Printf("Error sending SIGKILL to Factorio process: %s", err)
return err
}
server.SetRunning(false)
log.Printf("Sent SIGKILL to Factorio process. Factorio forced to exit.")
err = server.Rcon.Close()
if err != nil {
log.Printf("Error close rcon connection: %s", err)
}
return nil
}
func (server *Server) Stop() error {
err := server.Cmd.Process.Signal(os.Interrupt)
if err != nil {
if err.Error() == "os: process already finished" {
server.SetRunning(false)
return err
}
log.Printf("Error sending SIGINT to Factorio process: %s", err)
return err
}
server.SetRunning(false)
log.Printf("Sent SIGINT to Factorio process. Factorio shutting down...")
err = server.Rcon.Close()
if err != nil {
log.Printf("Error close rcon connection: %s", err)
}
return nil
}

View File

@ -0,0 +1,82 @@
package factorio
import (
"log"
"os"
"syscall"
"time"
)
func sendCtrlCToPid(pid int) {
d, e := syscall.LoadDLL("kernel32.dll")
if e != nil {
log.Fatalf("LoadDLL: %v\n", e)
}
p, e := d.FindProc("GenerateConsoleCtrlEvent")
if e != nil {
log.Fatalf("FindProc: %v\n", e)
}
r, _, e := p.Call(uintptr(syscall.CTRL_C_EVENT), uintptr(pid))
if r == 0 {
log.Fatalf("GenerateConsoleCtrlEvent: %v\n", e)
}
}
func setCtrlHandlingIsDisabledForThisProcess(disabled bool) {
disabledInt := 0
if disabled {
disabledInt = 1
}
d, e := syscall.LoadDLL("kernel32.dll")
if e != nil {
log.Fatalf("LoadDLL: %v\n", e)
}
p, e := d.FindProc("SetConsoleCtrlHandler")
if e != nil {
log.Fatalf("FindProc: %v\n", e)
}
r, _, e := p.Call(uintptr(0), uintptr(disabledInt))
if r == 0 {
log.Fatalf("SetConsoleCtrlHandler: %v\n", e)
}
}
func (server *Server) Kill() error {
err := server.Cmd.Process.Signal(os.Kill)
if err != nil {
if err.Error() == "os: process already finished" {
server.SetRunning(false)
return err
}
log.Printf("Error sending SIGKILL to Factorio process: %s", err)
return err
}
server.SetRunning(false)
log.Println("Sent SIGKILL to Factorio process. Factorio forced to exit.")
return nil
}
func (server *Server) Stop() error {
// Disable our own handling of CTRL+C, so we don't close when we send it to the console.
setCtrlHandlingIsDisabledForThisProcess(true)
// Send CTRL+C to all processes attached to the console (ourself, and the factorio server instance)
sendCtrlCToPid(0)
log.Println("Sent SIGINT to Factorio process. Factorio shutting down...")
// Somehow, the Factorio devs managed to code the game to react appropriately to CTRL+C, including
// saving the game, but not actually exit. So, we still have to manually kill the process, and
// for extra fun, there's no way to know when the server save has actually completed (unless we want
// to inject filesystem logic into what should be a process-level Stop() routine), so our best option
// is to just wait an arbitrary amount of time and hope that the save is successful in that time.
time.Sleep(2 * time.Second)
server.Cmd.Process.Signal(os.Kill)
// Re-enable handling of CTRL+C after we're sure that the factrio server is shut down.
setCtrlHandlingIsDisabledForThisProcess(false)
server.SetRunning(false)
return nil
}

View File

@ -1,4 +1,4 @@
package main
package factorio
import (
"encoding/binary"

View File

@ -1,277 +0,0 @@
package main
import (
"testing"
)
func Test0_17(t *testing.T) {
file, err := OpenArchiveFile("factorio_save_testfiles/test_0_17.zip", "level.dat")
if err != nil {
t.Fatalf("Error opening level.dat: %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{0,17,1,1},
Campaign: "transport-belt-madness",
Name: "level-01",
BaseMod: "base",
Difficulty: 0,
Finished: false,
PlayerWon: false,
NextLevel: "",
CanContinue: false,
FinishedButContinuing: false,
SavingReplay: true,
AllowNonAdminDebugOptions: true,
LoadedFrom: Version{0,17,1},
LoadedFromBuild: 43001,
AllowedCommands: 1,
Mods: []Mod {
{
Version: Version{0,2,0},
Name: "Warehousing",
},
{
Version: Version{0,17,1},
Name: "base",
},
},
}
header.Equals(testHeader, t)
}
func Test0_16(t *testing.T) {
file, err := OpenArchiveFile("factorio_save_testfiles/test_0_16.zip", "level.dat")
if err != nil {
t.Fatalf("Error opening level.dat: %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{0,16,51,0},
Campaign: "transport-belt-madness",
Name: "level-01",
BaseMod: "base",
Difficulty: 0,
Finished: false,
PlayerWon: false,
NextLevel: "",
CanContinue: false,
FinishedButContinuing: false,
SavingReplay: true,
AllowNonAdminDebugOptions: true,
LoadedFrom: Version{0,16,51},
LoadedFromBuild: 36654,
AllowedCommands: 1,
Mods: []Mod {
{
Version: Version{0,1,3},
Name: "Warehousing",
},
{
Version: Version{0,16,51},
Name: "base",
},
},
}
header.Equals(testHeader, t)
}
func Test0_15(t *testing.T) {
file, err := OpenArchiveFile("factorio_save_testfiles/test_0_15.zip", "level.dat")
if err != nil {
t.Fatalf("Error opening level.dat: %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{0,15,40,0},
Campaign: "transport-belt-madness",
Name: "level-01",
BaseMod: "base",
Difficulty: 0,
Finished: false,
PlayerWon: false,
NextLevel: "",
CanContinue: false,
FinishedButContinuing: false,
SavingReplay: true,
LoadedFrom: Version{0,15,40},
LoadedFromBuild: 30950,
AllowedCommands: 1,
Mods: []Mod {
{
Version: Version{0,0,13},
Name: "Warehousing",
},
{
Version: Version{0,15,40},
Name: "base",
},
},
}
header.Equals(testHeader, t)
}
func Test0_14(t *testing.T) {
file, err := OpenArchiveFile("factorio_save_testfiles/test_0_14.zip", "level.dat")
if err != nil {
t.Fatalf("Error opening level.dat: %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{0,14,23,0},
Campaign: "transport-belt-madness",
Name: "level-01",
BaseMod: "base",
Difficulty: 1,
Finished: false,
PlayerWon: false,
NextLevel: "",
CanContinue: false,
FinishedButContinuing: false,
SavingReplay: true,
LoadedFrom: Version{0,14,23},
LoadedFromBuild: 25374,
AllowedCommands: 1,
Mods: []Mod {
{
Version: Version{0,0,11},
Name: "Warehousing",
},
{
Version: Version{0,14,23},
Name: "base",
},
},
}
header.Equals(testHeader, t)
}
func Test0_13(t *testing.T) {
file, err := OpenArchiveFile("factorio_save_testfiles/test_0_13.zip", "level.dat")
if err != nil {
t.Fatalf("Error opening level.dat: %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{0,13,20,0},
Campaign: "transport-belt-madness",
Name: "level-01",
BaseMod: "base",
Difficulty: 1,
Finished: false,
PlayerWon: false,
NextLevel: "",
CanContinue: false,
FinishedButContinuing: false,
SavingReplay: true,
LoadedFrom: Version{0,13,20},
LoadedFromBuild: 24011,
AllowedCommands: 1,
Mods: []Mod {
{
Version: Version{1,1,0},
Name: "Extra-Virtual-Signals",
},
{
Version: Version{0,13,20},
Name: "base",
},
},
}
header.Equals(testHeader, t)
}
func (h *SaveHeader) Equals(other SaveHeader, t *testing.T) {
if h.FactorioVersion != other.FactorioVersion {
t.Errorf("FactorioVersion not equal: %s --- %s", h.FactorioVersion, other.FactorioVersion)
}
if h.Campaign != other.Campaign {
t.Errorf("Campaign not equal: %s --- %s", h.Campaign, other.Campaign)
}
if h.Name != other.Name {
t.Errorf("Name not equal: %s --- %s", h.Name, other.Name)
}
if h.BaseMod != other.BaseMod {
t.Errorf("BaseMod not equal: %s --- %s", h.BaseMod, other.BaseMod)
}
if h.Difficulty != other.Difficulty {
t.Errorf("Difficulty not equal: %d --- %d", h.Difficulty, other.Difficulty)
}
if h.Finished != other.Finished {
t.Errorf("Finished not equal: %t --- %t", h.Finished, other.Finished)
}
if h.PlayerWon != other.PlayerWon {
t.Errorf("PlayerWon not equal: %t --- %t", h.PlayerWon, other.PlayerWon)
}
if h.NextLevel != other.NextLevel {
t.Errorf("NextLevel not equal: %s --- %s", h.NextLevel, other.NextLevel)
}
if h.CanContinue != other.CanContinue {
t.Errorf("CanContinue not equal: %t --- %t", h.CanContinue, other.CanContinue)
}
if h.FinishedButContinuing != other.FinishedButContinuing {
t.Errorf("FinishedButContinuing not equal: %t --- %t", h.FinishedButContinuing, other.FinishedButContinuing)
}
if h.SavingReplay != other.SavingReplay {
t.Errorf("SavingReplay not equal: %t --- %t", h.SavingReplay, other.SavingReplay)
}
if h.AllowNonAdminDebugOptions != other.AllowNonAdminDebugOptions {
t.Errorf("AllowNonAdminDebugOptions not equal: %t --- %t", h.AllowNonAdminDebugOptions, other.AllowNonAdminDebugOptions)
}
if h.LoadedFrom != other.LoadedFrom {
t.Errorf("LoadedFrom not equal: %s --- %s", h.LoadedFrom, other.LoadedFrom)
}
if h.LoadedFromBuild != other.LoadedFromBuild {
t.Errorf("LoadedFromBuild not equal: %d --- %d", h.LoadedFromBuild, other.LoadedFromBuild)
}
if h.AllowedCommands != other.AllowedCommands {
t.Errorf("AllowedCommands not equal: %d --- %d", h.AllowedCommands, other.AllowedCommands)
}
for k := range h.Mods {
if h.Mods[k].Name != other.Mods[k].Name {
t.Errorf("ModNames not equal: %s --- %s", h.Mods[k].Name, other.Mods[k].Name)
} else if h.Mods[k].Version != other.Mods[k].Version {
t.Errorf("ModVersions of Mod %s are not equal: %s --- %s", h.Mods[k].Name, h.Mods[k].Version, other.Mods[k].Version)
}
}
}

View File

@ -1,9 +0,0 @@
package main
// Stubs for windows-only functions
func sendCtrlCToPid(pid int) {
}
func setCtrlHandlingIsDisabledForThisProcess(disabled bool) {
}

View File

@ -1,41 +0,0 @@
package main
import (
"log"
"syscall"
)
func sendCtrlCToPid(pid int) {
d, e := syscall.LoadDLL("kernel32.dll")
if e != nil {
log.Fatalf("LoadDLL: %v\n", e)
}
p, e := d.FindProc("GenerateConsoleCtrlEvent")
if e != nil {
log.Fatalf("FindProc: %v\n", e)
}
r, _, e := p.Call(uintptr(syscall.CTRL_C_EVENT), uintptr(pid))
if r == 0 {
log.Fatalf("GenerateConsoleCtrlEvent: %v\n", e)
}
}
func setCtrlHandlingIsDisabledForThisProcess(disabled bool) {
disabledInt := 0
if disabled {
disabledInt = 1
}
d, e := syscall.LoadDLL("kernel32.dll")
if e != nil {
log.Fatalf("LoadDLL: %v\n", e)
}
p, e := d.FindProc("SetConsoleCtrlHandler")
if e != nil {
log.Fatalf("FindProc: %v\n", e)
}
r, _, e := p.Call(uintptr(0), uintptr(disabledInt))
if r == 0 {
log.Fatalf("SetConsoleCtrlHandler: %v\n", e)
}
}

Binary file not shown.

View File

@ -0,0 +1 @@
The usage of the belt-balancer mod is allowed without additional licensing by knoxfighter/asdff45 (the creator of the mod and also contributor to this factorio-server-manager project)

Binary file not shown.

Binary file not shown.

View File

@ -10,10 +10,13 @@ require (
github.com/gorilla/sessions v1.2.0 // indirect
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

View File

@ -1,5 +1,7 @@
github.com/apexskier/httpauth v1.3.2 h1:PHwrq/eBRBLIrUthchpbDVTVR/ofBrj2LUcukCRhfXw=
github.com/apexskier/httpauth v1.3.2/go.mod h1:aEHd6x648VCocEK0vTsPKkjJ1sBPab3Z4V4MJs9YZAE=
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=
@ -24,6 +26,10 @@ github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvK
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/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=
@ -37,10 +43,15 @@ 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=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -78,3 +89,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
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=

View File

@ -1,791 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
"github.com/gorilla/mux"
)
type JSONResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,string"`
}
type JSONResponseFileInput struct {
Success bool `json:"success"`
Data interface{} `json:"data,string"`
Error string `json:"error"`
ErrorKeys []int `json:"errorkeys"`
}
// Lists all save files in the factorio/saves directory
func ListSaves(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
savesList, err := listSaves(config.FactorioSavesDir)
if err != nil {
resp.Success = false
resp.Data = fmt.Sprintf("Error listing save files: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing saves: %s", err)
}
return
}
loadLatest := Save{Name: "Load Latest"}
savesList = append(savesList, loadLatest)
resp.Data = savesList
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing saves: %s", err)
}
}
func DLSave(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/octet-stream")
vars := mux.Vars(r)
save := vars["save"]
saveName := filepath.Join(config.FactorioSavesDir, save)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", save))
log.Printf("%s downloading: %s", r.Host, saveName)
http.ServeFile(w, r, saveName)
}
func UploadSave(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
switch r.Method {
case "GET":
resp.Data = "Unsupported method"
resp.Success = false
if err = json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing mods: %s", err)
}
case "POST":
log.Println("Uploading save file")
r.ParseMultipartForm(32 << 20)
for _, saveFile := range r.MultipartForm.File["savefile"] {
file, err := saveFile.Open()
if err != nil {
resp.Success = false
resp.Data = err.Error()
json.NewEncoder(w).Encode(resp)
log.Printf("Error in upload save formfile: %s", err.Error())
return
}
defer file.Close()
out, err := os.Create(filepath.Join(config.FactorioSavesDir, saveFile.Filename))
if err != nil {
resp.Success = false
resp.Data = err.Error()
json.NewEncoder(w).Encode(resp)
log.Printf("Error in out: %s", err)
return
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
resp.Success = false
resp.Data = err.Error()
json.NewEncoder(w).Encode(resp)
log.Printf("Error in io copy: %s", err)
return
}
log.Printf("Uploaded save file: %s", saveFile.Filename)
resp.Data = "File '" + saveFile.Filename + "' uploaded successfully"
resp.Success = true
json.NewEncoder(w).Encode(resp)
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
// Deletes provided save
func RemoveSave(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
vars := mux.Vars(r)
name := vars["save"]
save, err := findSave(name)
if err != nil {
resp.Data = fmt.Sprintf("Error removing save: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error removing save %s", err)
}
}
err = save.remove()
if err == nil {
// save was removed
resp.Data = fmt.Sprintf("Removed save: %s", save.Name)
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error removing save %s", err)
}
} else {
log.Printf("Error in remove save handler: %s", err)
resp.Data = fmt.Sprintf("Error in remove save handler: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error removing save: %s", err)
}
}
}
// Launches Factorio server binary with --create flag to create save
// Url must include save name for creation of savefile
func CreateSaveHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
vars := mux.Vars(r)
saveName := vars["save"]
if saveName == "" {
log.Printf("Error creating save, no name provided: %s", err)
resp.Data = "No save name provided."
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding save handler response: %s", err)
}
return
}
saveFile := filepath.Join(config.FactorioSavesDir, saveName)
cmdOut, err := createSave(saveFile)
if err != nil {
log.Printf("Error creating save: %s", err)
resp.Data = "Error creating savefile."
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding save handler response: %s", err)
}
return
}
resp.Success = true
resp.Data = fmt.Sprintf("Save %s created successfully. Command output: \n%s", saveName, cmdOut)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding save response: %s", err)
}
}
// LogTail returns last lines of the factorio-current.log file
func LogTail(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
resp.Data, err = tailLog(config.FactorioLog)
if err != nil {
resp.Data = fmt.Sprintf("Could not tail %s: %s", config.FactorioLog, err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Could not tail %s: %s", config.FactorioLog, err)
}
return
}
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error tailing logfile: %s", err)
}
}
// LoadConfig returns JSON response of config.ini file
func LoadConfig(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
configContents, err := loadConfig(config.FactorioConfigFile)
if err != nil {
log.Printf("Could not retrieve config.ini: %s", err)
resp.Data = "Error getting config.ini"
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error tailing logfile: %s", err)
}
} else {
resp.Data = configContents
resp.Success = true
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: %s", err)
}
log.Printf("Sent config.ini response")
}
func StartServer(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
if FactorioServ.Running {
resp.Data = "Factorio server is already running"
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding JSON response: %s", err)
}
return
}
switch r.Method {
case "GET":
log.Printf("GET not supported for startserver handler")
resp.Data = "Unsupported method"
resp.Success = false
if err = json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing mods: %s", err)
}
case "POST":
log.Printf("Starting Factorio server.")
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error in starting factorio server handler body: %s", err)
return
}
log.Printf("Starting Factorio server with settings: %v", string(body))
err = json.Unmarshal(body, &FactorioServ)
if err != nil {
log.Printf("Error unmarshaling server settings JSON: %s", err)
return
}
// Check if savefile was submitted with request to start server.
if FactorioServ.Savefile == "" {
log.Printf("Error starting Factorio server: no save file provided")
resp.Success = false
resp.Data = fmt.Sprintf("Error starting Factorio server: %s", "No save file provided")
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: %s", err)
}
return
}
go func() {
err = FactorioServ.Run()
if err != nil {
log.Printf("Error starting Factorio server: %+v", err)
return
}
}()
timeout := 0
for timeout <= 3 {
time.Sleep(1 * time.Second)
if FactorioServ.Running {
resp.Data = fmt.Sprintf("Factorio server with save: %s started on port: %d", FactorioServ.Savefile, FactorioServ.Port)
resp.Success = true
log.Printf("Factorio server started on port: %v", FactorioServ.Port)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: %s", err)
}
break
} else {
log.Printf("Did not detect running Factorio server attempt: %+v", timeout)
}
timeout++
}
if FactorioServ.Running == false {
log.Printf("Error starting Factorio server: %s", err)
resp.Data = fmt.Sprintf("Error starting Factorio server: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding start server JSON response: %s", err)
}
}
}
}
func StopServer(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
if FactorioServ.Running {
err := FactorioServ.Stop()
if err != nil {
log.Printf("Error in stop server handler: %s", err)
resp.Data = fmt.Sprintf("Error in stop server handler: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: %s", err)
}
return
}
log.Printf("Stopped Factorio server.")
resp.Success = true
resp.Data = fmt.Sprintf("Factorio server stopped")
} else {
resp.Data = "Factorio server is not running"
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: %s", err)
}
return
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: %s", err)
}
}
func KillServer(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
if FactorioServ.Running {
err := FactorioServ.Kill()
if err != nil {
log.Printf("Error in kill server handler: %s", err)
resp.Data = fmt.Sprintf("Error in kill server handler: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: %s", err)
}
return
}
log.Printf("Killed Factorio server.")
resp.Success = true
resp.Data = fmt.Sprintf("Factorio server killed")
} else {
resp.Data = "Factorio server is not running"
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: %s", err)
}
return
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: %s", err)
}
}
func CheckServer(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
if FactorioServ.Running {
resp.Success = true
status := map[string]string{}
status["status"] = "running"
status["port"] = strconv.Itoa(FactorioServ.Port)
status["savefile"] = FactorioServ.Savefile
status["address"] = FactorioServ.BindIP
resp.Data = status
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: %s", err)
}
} else {
resp.Success = true
status := map[string]string{}
status["status"] = "stopped"
resp.Data = status
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: %s", err)
}
}
}
func FactorioVersion(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: true,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
status := map[string]string{}
status["version"] = FactorioServ.Version.String()
status["base_mod_version"] = FactorioServ.BaseModVersion
resp.Data = status
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error loading Factorio Version: %s", err)
}
}
func LoginUser(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
switch r.Method {
case "GET":
log.Printf("GET not supported for login handler")
resp.Data = "Unsupported method"
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing mods: %s", err)
}
case "POST":
var user User
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error in starting factorio server handler body: %s", err)
return
}
err = json.Unmarshal(body, &user)
if err != nil {
log.Printf("Error unmarshaling server settings JSON: %s", err)
return
}
log.Printf("Logging in user: %s", user.Username)
err = Auth.aaa.Login(w, r, user.Username, user.Password, "/")
if err != nil {
log.Printf("Error logging in user: %s, error: %s", user.Username, err)
resp.Data = fmt.Sprintf("Error logging in user: %s", user.Username)
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing mods: %s", err)
}
return
}
log.Printf("User: %s, logged in successfully", user.Username)
resp.Data = fmt.Sprintf("User: %s, logged in successfully", user.Username)
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing mods: %s", err)
}
}
}
func LogoutUser(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
if err := Auth.aaa.Logout(w, r); err != nil {
log.Printf("Error logging out current user")
return
}
resp.Success = true
resp.Data = "User logged out successfully."
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error logging out: %s", err)
}
}
func GetCurrentLogin(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
user, err := Auth.aaa.CurrentUser(w, r)
if err != nil {
log.Printf("Error getting current user status: %s", err)
resp.Data = fmt.Sprintf("Error getting user status: %s", user.Username)
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing mods: %s", err)
}
return
}
resp.Success = true
resp.Data = user
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error getting user status: %s", err)
}
}
func ListUsers(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
users, err := Auth.listUsers()
if err != nil {
log.Printf("Error in ListUsers handler: %s", err)
resp.Data = fmt.Sprint("Error listing users")
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing mods: %s", err)
}
return
}
resp.Success = true
resp.Data = users
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error getting user status: %s", err)
}
}
func AddUser(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
switch r.Method {
case "GET":
log.Printf("GET not supported for add user handler")
resp.Data = "Unsupported method"
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error adding user: %s", err)
}
case "POST":
user := User{}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error in reading add user POST: %s", err)
resp.Data = fmt.Sprintf("Error in adding user: %s", err)
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error adding user: %s", err)
}
return
}
log.Printf("Adding user: %v", string(body))
err = json.Unmarshal(body, &user)
if err != nil {
log.Printf("Error unmarshaling user add JSON: %s", err)
resp.Data = fmt.Sprintf("Error in adding user: %s", err)
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error adding user: %s", err)
}
return
}
err = Auth.addUser(user.Username, user.Password, user.Email, user.Role)
if err != nil {
log.Printf("Error in adding user: %s", err)
resp.Data = fmt.Sprintf("Error in adding user: %s", err)
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error adding user: %s", err)
}
return
}
resp.Success = true
resp.Data = fmt.Sprintf("User: %s successfully added.", user.Username)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in returning added user response: %s", err)
}
}
}
func RemoveUser(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
switch r.Method {
case "GET":
log.Printf("GET not supported for add user handler")
resp.Data = "Unsupported method"
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error adding user: %s", err)
}
case "POST":
user := User{}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error in reading remove user POST: %s", err)
resp.Data = fmt.Sprintf("Error in removing user: %s", err)
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error adding user: %s", err)
}
return
}
err = json.Unmarshal(body, &user)
if err != nil {
log.Printf("Error unmarshaling user remove JSON: %s", err)
resp.Data = fmt.Sprintf("Error in removing user: %s", err)
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error removing user: %s", err)
}
return
}
err = Auth.removeUser(user.Username)
if err != nil {
log.Printf("Error in remove user handler: %s", err)
resp.Data = fmt.Sprintf("Error in removing user: %s", err)
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error adding user: %s", err)
}
return
}
resp.Success = true
resp.Data = fmt.Sprintf("User: %s successfully removed.", user.Username)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in returning remove user response: %s", err)
}
}
}
// GetServerSettings returns JSON response of server-settings.json file
func GetServerSettings(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
resp.Data = FactorioServ.Settings
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding server settings JSON reponse: %s", err)
}
log.Printf("Sent server settings response")
}
func UpdateServerSettings(w http.ResponseWriter, r *http.Request) {
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
switch r.Method {
case "GET":
log.Printf("GET not supported for add user handler")
resp.Data = "Unsupported method"
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error adding user: %s", err)
}
case "POST":
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error in reading server settings POST: %s", err)
resp.Data = fmt.Sprintf("Error in updating settings: %s", err)
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error updating settings: %s", err)
}
return
}
log.Printf("Received settings JSON: %s", body)
err = json.Unmarshal(body, &FactorioServ.Settings)
if err != nil {
log.Printf("Error unmarshaling server settings JSON: %s", err)
resp.Data = fmt.Sprintf("Error in updating settings: %s", err)
resp.Success = false
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding server settings response: %s", err)
}
return
}
settings, err := json.MarshalIndent(&FactorioServ.Settings, "", " ")
if err != nil {
log.Printf("Failed to marshal server settings: %s", err)
return
} else {
if err = ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.SettingsFile), settings, 0644); err != nil {
log.Printf("Failed to save server settings: %v\n", err)
return
}
log.Printf("Saved Factorio server settings in server-settings.json")
}
if(FactorioServ.Version.Greater(Version{0,17,0})) {
// save admins to adminJson
admins, err := json.MarshalIndent(FactorioServ.Settings["admins"], "", " ")
if err != nil {
log.Printf("Failed to marshal admins-Setting: %s", err)
return
}
err = ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile), admins, 0664)
if err != nil {
log.Printf("Failed to save admins: %s", err)
return
}
}
resp.Success = true
resp.Data = fmt.Sprintf("Settings successfully saved")
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in sending server settings response: %s", err)
}
}
}

View File

@ -1,136 +1,34 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"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"
"path/filepath"
"runtime"
)
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"`
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
}
var (
config Config
FactorioServ *FactorioServer
Auth *AuthHTTP
)
func failOnError(err error, msg string) {
if err != nil {
log.Printf("%s: %s", msg, err)
panic(fmt.Sprintf("%s: %s", msg, err))
}
}
// Loads server configuration files
// JSON config file contains default values,
// config file will overwrite any provided flags
func loadServerConfig(f string) {
file, err := os.Open(f)
failOnError(err, "Error loading config file.")
decoder := json.NewDecoder(file)
err = decoder.Decode(&config)
failOnError(err, "Error decoding JSON config file.")
config.FactorioRconPort = randomPort()
}
func parseFlags() {
confFile := flag.String("conf", "./conf.json", "Specify location of Factorio Server Manager config file.")
factorioDir := flag.String("dir", "./", "Specify location of Factorio directory.")
serverIP := flag.String("host", "0.0.0.0", "Specify IP for webserver to listen on.")
factorioIP := flag.String("game-bind-address", "0.0.0.0", "Specify IP for Fcatorio gamer server to listen on.")
factorioPort := flag.String("port", "8080", "Specify a port for the server.")
factorioConfigFile := flag.String("config", "config/config.ini", "Specify location of Factorio config.ini file")
factorioMaxUpload := flag.Int64("max-upload", 1024*1024*20, "Maximum filesize for uploaded files (default 20MB).")
factorioBinary := flag.String("bin", "bin/x64/factorio", "Location of Factorio Server binary file")
glibcCustom := flag.String("glibc-custom", "false", "By default false, if custom glibc is required set this to true and add glibc-loc and glibc-lib-loc parameters")
glibcLocation := flag.String("glibc-loc", "/opt/glibc-2.18/lib/ld-2.18.so", "Location glibc ld.so file if needed (ex. /opt/glibc-2.18/lib/ld-2.18.so)")
glibcLibLoc := flag.String("glibc-lib-loc", "/opt/glibc-2.18/lib", "Location of glibc lib folder (ex. /opt/glibc-2.18/lib)")
flag.Parse()
config.glibcCustom = *glibcCustom
config.glibcLocation = *glibcLocation
config.glibcLibLoc = *glibcLibLoc
config.ConfFile = *confFile
config.FactorioDir = *factorioDir
config.ServerIP = *serverIP
config.FactorioIP = *factorioIP
config.ServerPort = *factorioPort
config.FactorioSavesDir = filepath.Join(config.FactorioDir, "saves")
config.FactorioModsDir = filepath.Join(config.FactorioDir, "mods")
config.FactorioModPackDir = "./mod_packs"
config.FactorioConfigDir = filepath.Join(config.FactorioDir, "config")
config.FactorioConfigFile = filepath.Join(config.FactorioDir, *factorioConfigFile)
config.FactorioBinary = filepath.Join(config.FactorioDir, *factorioBinary)
config.FactorioCredentialsFile = "./factorio.auth"
config.FactorioAdminFile = "server-adminlist.json"
config.MaxUploadSize = *factorioMaxUpload
if runtime.GOOS == "windows" {
appdata := os.Getenv("APPDATA")
config.FactorioLog = filepath.Join(appdata, "Factorio", "factorio-current.log")
} else {
config.FactorioLog = filepath.Join(config.FactorioDir, "factorio-current.log")
}
}
func main() {
var err error
// get the all configs based on the flags
config := bootstrap.NewConfig(os.Args)
// Parse configuration flags
parseFlags()
// Load server config from file
loadServerConfig(config.ConfFile)
// create mod-stuff
modStartUp()
// setup the required files for the mods
factorio.ModStartUp()
// Initialize Factorio Server struct
FactorioServ, err = initFactorio()
err := factorio.NewFactorioServer()
if err != nil {
log.Printf("Error occurred during FactorioServer initializaion: %v\n", err)
log.Printf("Error occurred during Server initializaion: %v\n", err)
return
}
// Initialize authentication system
Auth = initAuth()
Auth.CreateAuth(config.DatabaseFile, config.CookieEncryptionKey)
Auth.CreateOrUpdateUser(config.Username, config.Password, "admin", "")
api.GetAuth()
// Initialize HTTP router -- also initializes websocket
router := api.NewRouter()
// Initialize HTTP router
router := NewRouter()
log.Printf("Starting server on: %s:%s", config.ServerIP, config.ServerPort)
log.Fatal(http.ListenAndServe(config.ServerIP+":"+config.ServerPort, router))

View File

@ -1,923 +0,0 @@
package main
import (
"archive/zip"
"encoding/json"
"errors"
"fmt"
"github.com/mroote/factorio-server-manager/lockfile"
"io"
"log"
"net/http"
"os"
"path/filepath"
"time"
"github.com/gorilla/mux"
)
type ModPortalStruct struct {
DownloadsCount int `json:"downloads_count"`
Name string `json:"name"`
Owner string `json:"owner"`
Releases []struct {
DownloadURL string `json:"download_url"`
FileName string `json:"file_name"`
InfoJSON struct {
FactorioVersion string `json:"factorio_version"`
} `json:"info_json"`
ReleasedAt time.Time `json:"released_at"`
Sha1 string `json:"sha1"`
Version Version `json:"version"`
} `json:"releases"`
Summary string `json:"summary"`
Title string `json:"title"`
}
// Returns JSON response of all mods installed in factorio/mods
func listInstalledModsHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
mods, err := newMods(config.FactorioModsDir)
if err != nil {
w.WriteHeader(500)
resp.Data = fmt.Sprintf("Error in ListInstalledMods handler: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in list mods: %s", err)
}
return
}
resp.Data = mods.listInstalledMods().ModsResult
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in list mods: %s", err)
}
}
// LoginFactorioModPortal returns JSON response with success or error-message
func LoginFactorioModPortal(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
username := r.FormValue("username")
password := r.FormValue("password")
loginStatus, err, statusCode := factorioLogin(username, password)
if loginStatus == "" && err == nil {
resp.Data = true
}
w.WriteHeader(statusCode)
if err != nil {
resp.Data = fmt.Sprintf("Error trying to login into Factorio: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in Factorio-Login: %s", err)
}
return
}
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in Factorio-Login: %s", err)
}
}
func LoginstatusFactorioModPortal(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
var credentials FactorioCredentials
resp.Data, err = credentials.load()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error getting the factorio credentials: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in Factorio-Login: %s", err)
}
return
}
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in Factorio-Login: %s", err)
}
}
func LogoutFactorioModPortalHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
var credentials FactorioCredentials
err = credentials.del()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error on logging out of factorio: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in Factorio-Login: %s", err)
}
return
}
resp.Data = false
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in Factorio-Login: %s", err)
}
}
//ModPortalSearchHandler returns JSON response with the found mods
func ModPortalSearchHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//Get Data out of the request
searchKeyword := r.FormValue("search")
var statusCode int
resp.Data, err, statusCode = searchModPortal(searchKeyword)
w.WriteHeader(statusCode)
if err != nil {
w.WriteHeader(500)
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in searchModPortal: %s", err)
}
return
}
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in ModPortalSearch: %s", err)
}
}
//ModPortalDetailsHandler returns JSON response with the mod details
func ModPortalDetailsHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//Get Data out of the request
modId := r.FormValue("modId")
var statusCode int
resp.Data, err, statusCode = getModDetails(modId)
w.WriteHeader(statusCode)
if err != nil {
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in searchModPortal: %s", err)
}
return
}
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in ModPortalSearch: %s", err)
}
}
func ModPortalInstallHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//Get Data out of the request
downloadUrl := r.FormValue("link")
filename := r.FormValue("filename")
modName := r.FormValue("modName")
mods, err := newMods(config.FactorioModsDir)
if err == nil {
err = mods.downloadMod(downloadUrl, filename, modName)
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in installMod: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in installMod: %s", err)
}
return
}
resp.Data = mods.listInstalledMods()
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in ModPortalInstallHandler: %s", err)
}
}
func ModPortalInstallMultipleHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
r.ParseForm()
var modsList []string
var versionsList []Version
//Parse incoming data
for key, values := range r.PostForm {
if key == "mod_name" {
for _, v := range values {
modsList = append(modsList, v)
}
} else if key == "mod_version" {
for _, value := range values {
var v Version
if err := v.UnmarshalText([]byte(value)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in searchModPortal: %s", err)
}
}
versionsList = append(versionsList, v)
}
}
}
mods, err := newMods(config.FactorioModsDir)
if err != nil {
log.Printf("error creating mods: %s", err)
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in searchModPortal: %s", err)
}
return
}
for modIndex, mod := range modsList {
var err error
//get details of mod
modDetails, err, statusCode := getModDetails(mod)
if err != nil {
w.WriteHeader(statusCode)
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in searchModPortal: %s", err)
}
return
}
modDetailsArray := []byte(modDetails)
var modDetailsStruct ModPortalStruct
//read mod-data into Struct
err = json.Unmarshal(modDetailsArray, &modDetailsStruct)
if err != nil {
log.Printf("error reading modPortalDetails: %s", err)
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in searchModPortal: %s", err)
}
return
}
//find correct mod-version
for _, release := range modDetailsStruct.Releases {
if release.Version.Equals(versionsList[modIndex]) {
mods.downloadMod(release.DownloadURL, release.FileName, modDetailsStruct.Name)
break
}
}
}
resp.Data = mods.listInstalledMods()
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in ToggleModHandler: %s", err)
}
}
func ToggleModHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//Get Data out of the request
modName := r.FormValue("modName")
mods, err := newMods(config.FactorioModsDir)
if err == nil {
err, resp.Data = mods.ModSimpleList.toggleMod(modName)
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in listInstalledModsByFolder: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in listInstalledModsByFolder: %s", err)
}
return
}
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in ToggleModHandler: %s", err)
}
}
func DeleteModHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//Get Data out of the request
modName := r.FormValue("modName")
mods, err := newMods(config.FactorioModsDir)
if err == nil {
mods.deleteMod(modName)
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in deleteMod: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in DeleteModHandler: %s", err)
}
return
}
resp.Data = modName
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in DeleteModHandler: %s", err)
}
}
func DeleteAllModsHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//delete mods folder
err = deleteAllMods()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in deleteMod: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in DeleteModHandler: %s", err)
}
return
}
resp.Data = nil
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in DeleteModHandler: %s", err)
}
}
func UpdateModHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//Get Data out of the request
modName := r.FormValue("modName")
downloadUrl := r.FormValue("downloadUrl")
fileName := r.FormValue("filename")
log.Println("--------------------------------------------------------------")
mods, err := newMods(config.FactorioModsDir)
if err == nil {
err = mods.updateMod(modName, downloadUrl, fileName)
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in deleteMod: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in DeleteModHandler: %s", err)
}
return
}
installedMods := mods.listInstalledMods().ModsResult
for _, mod := range installedMods {
if mod.Name == modName {
resp.Data = mod
break
}
}
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in DeleteModHandler: %s", err)
}
}
func UploadModHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponseFileInput{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
r.ParseMultipartForm(32 << 20)
mods, err := newMods(config.FactorioModsDir)
if err == nil {
for fileKey, modFile := range r.MultipartForm.File["mod_file"] {
err = mods.uploadMod(modFile)
if err != nil {
resp.ErrorKeys = append(resp.ErrorKeys, fileKey)
resp.Error = "An error occurred during upload or saving, pls check manually, if all went well and delete invalid files. (This program also could be crashed)"
}
}
}
if err != nil {
w.WriteHeader(500)
resp.Data = fmt.Sprintf("Error in uploadMod, listing mods wasn't successful: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in uploadMod, listing mods wasn't successful: %s", err)
}
return
}
resp.Data = mods.listInstalledMods()
resp.Success = true
if err = json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in UploadModHandler: %s", err)
}
}
func DownloadModsHandler(w http.ResponseWriter, r *http.Request) {
var err error
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
//iterate over folder and create everything in the zip
err = filepath.Walk(config.FactorioModsDir, func(path string, info os.FileInfo, err error) error {
if info.IsDir() == false {
//Lock the file, that we are want to read
err := fileLock.RLock(path)
if err != nil {
log.Printf("error locking file for reading, something else has locked it")
return err
}
defer fileLock.RUnlock(path)
writer, err := zipWriter.Create(info.Name())
if err != nil {
log.Printf("error on creating new file inside zip: %s", err)
return err
}
file, err := os.Open(path)
if err != nil {
log.Printf("error on opening modfile: %s", err)
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
if err != nil {
log.Printf("error on copying file into zip: %s", err)
return err
}
err = file.Close()
if err != nil {
log.Printf("error closing file: %s", err)
return err
}
}
return nil
})
if err == lockfile.ErrorAlreadyLocked {
w.WriteHeader(http.StatusLocked)
return
}
if err != nil {
log.Printf("error on walking over the mods: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
writerHeader := w.Header()
writerHeader.Set("Content-Type", "application/zip;charset=UTF-8")
writerHeader.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", "all_installed_mods.zip"))
}
//LoadModsFromSaveHandler returns JSON response with the found mods
func LoadModsFromSaveHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//Get Data out of the request
SaveFile := r.FormValue("saveFile")
path := filepath.Join(config.FactorioSavesDir, SaveFile)
f, err := OpenArchiveFile(path, "level.dat")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("cannot open save level file: %v", err)
resp.Data = "Error opening save file"
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in loadModsFromSave: %s", err)
}
return
}
defer f.Close()
var header SaveHeader
err = header.ReadFrom(f)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("cannot read save header: %v", err)
resp.Data = "Error reading save file"
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in loadModsFromSave: %s", err)
}
return
}
resp.Data = header
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in LoadModsFromSave: %s", err)
}
}
func ListModPacksHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
modPackMap, err := newModPackMap()
if err != nil {
w.WriteHeader(500)
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error listing modpack files: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing modpacks: %s", err)
}
return
}
resp.Data = modPackMap.listInstalledModPacks()
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing saves: %s", err)
}
}
func CreateModPackHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
name := r.FormValue("name")
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
modPackMap, err := newModPackMap()
if err == nil {
err = modPackMap.createModPack(name)
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error creating modpack file: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error creating modpack: %s", err)
}
return
}
resp.Data = modPackMap.listInstalledModPacks()
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error creating modpack response: %s", err)
}
}
func DownloadModPackHandler(w http.ResponseWriter, r *http.Request) {
var err error
vars := mux.Vars(r)
modpack := vars["modpack"]
modPackMap, err := newModPackMap()
if err != nil {
log.Printf("error on loading modPacks: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if modPackMap.checkModPackExists(modpack) {
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
//iterate over folder and create everything in the zip
err = filepath.Walk(filepath.Join(config.FactorioModPackDir, modpack), func(path string, info os.FileInfo, err error) error {
if info.IsDir() == false {
writer, err := zipWriter.Create(info.Name())
if err != nil {
log.Printf("error on creating new file inside zip: %s", err)
return err
}
file, err := os.Open(path)
if err != nil {
log.Printf("error on opening modfile: %s", err)
return err
}
// Close file, when function returns
defer func() {
err2 := file.Close()
if err == nil && err2 != nil {
log.Printf("Error closing file: %s", err2)
err = err2
}
}()
_, err = io.Copy(writer, file)
if err != nil {
log.Printf("error on copying file into zip: %s", err)
return err
}
}
return nil
})
if err != nil {
log.Printf("error on walking over the modpack: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
} else {
log.Printf("requested modPack doesnt exist")
w.WriteHeader(http.StatusNotFound)
return
}
writerHeader := w.Header()
writerHeader.Set("Content-Type", "application/zip;charset=UTF-8")
writerHeader.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", modpack+".zip"))
}
func DeleteModPackHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
name := r.FormValue("name")
modPackMap, err := newModPackMap()
if err == nil {
err = modPackMap.deleteModPack(name)
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error deleting modpack file: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error deleting modpack: %s", err)
}
return
}
resp.Data = name
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error creating delete modpack response: %s", err)
}
}
func LoadModPackHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
name := r.FormValue("name")
modPackMap, err := newModPackMap()
if err == nil {
modPackMap[name].loadModPack()
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error loading modpack file: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error loading modpack: %s", err)
}
return
}
resp.Data = modPackMap[name].Mods.listInstalledMods()
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error creating loading modpack response: %s", err)
}
}
func ModPackToggleModHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
modName := r.FormValue("modName")
modPackName := r.FormValue("modPack")
modPackMap, err := newModPackMap()
if err == nil {
err, resp.Data = modPackMap[modPackName].Mods.ModSimpleList.toggleMod(modName)
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error loading modpack file: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error loading modpack: %s", err)
}
return
}
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error creating loading modpack response: %s", err)
}
}
func ModPackDeleteModHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
modName := r.FormValue("modName")
modPackName := r.FormValue("modPackName")
modPackMap, err := newModPackMap()
if err == nil {
if modPackMap.checkModPackExists(modPackName) {
err = modPackMap[modPackName].Mods.deleteMod(modName)
} else {
err = errors.New("ModPack " + modPackName + " does not exist")
}
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error loading modpack file: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error loading modpack: %s", err)
}
return
}
resp.Data = true
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error creating loading modpack response: %s", err)
}
return
}
func ModPackUpdateModHandler(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
//Get Data out of the request
modName := r.FormValue("modName")
downloadUrl := r.FormValue("downloadUrl")
fileName := r.FormValue("filename")
modPackName := r.FormValue("modPackName")
modPackMap, err := newModPackMap()
if err == nil {
if modPackMap.checkModPackExists(modPackName) {
err = modPackMap[modPackName].Mods.updateMod(modName, downloadUrl, fileName)
} else {
err = errors.New("ModPack " + modPackName + "does not exist")
}
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
resp.Data = fmt.Sprintf("Error in deleteMod: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in DeleteModHandler: %s", err)
}
return
}
installedMods := modPackMap[modPackName].Mods.listInstalledMods().ModsResult
for _, mod := range installedMods {
if mod.Name == modName {
resp.Data = mod
break
}
}
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in DeleteModHandler: %s", err)
}
}

View File

@ -1,58 +0,0 @@
package main
import (
"github.com/gorilla/websocket"
)
type Message struct {
Name string `json:"name"`
Data interface{} `json:"data"`
}
type FindHandler func(string) (Handler, bool)
type Client struct {
send chan Message
socket *websocket.Conn
findHandler FindHandler
stopChannels map[int]chan bool
id string
}
func (client *Client) Read() {
var message Message
for {
if err := client.socket.ReadJSON(&message); err != nil {
break
}
if handler, found := client.findHandler(message.Name); found {
handler(client, message.Data)
}
}
client.socket.Close()
}
func (client *Client) Write() {
for msg := range client.send {
if err := client.socket.WriteJSON(msg); err != nil {
break
}
}
client.socket.Close()
}
func (client *Client) Close() {
for _, ch := range client.stopChannels {
ch <- true
}
close(client.send)
}
func NewClient(socket *websocket.Conn, findHandler FindHandler) *Client {
return &Client{
send: make(chan Message),
socket: socket,
findHandler: findHandler,
stopChannels: make(map[int]chan bool),
}
}

View File

@ -1,41 +0,0 @@
package main
import (
"log"
"path/filepath"
"github.com/hpcloud/tail"
)
func logSubscribe(client *Client, data interface{}) {
go func() {
logfile := filepath.Join(config.FactorioDir, "factorio-server-console.log")
t, err := tail.TailFile(logfile, tail.Config{Follow: true})
if err != nil {
log.Printf("Error subscribing to tail log %s", err)
return
}
for line := range t.Lines {
client.send <- Message{"log update", line.Text}
}
}()
}
func commandSend(client *Client, data interface{}) {
if FactorioServ.Running {
go func() {
log.Printf("Received command: %v", data)
reqId, err := FactorioServ.Rcon.Write(data.(string))
if err != nil {
log.Printf("Error sending rcon command: %s", err)
return
}
log.Printf("Command send to Factorio: %s, with rcon request id: %v", data, reqId)
client.send <- Message{"receive command", data}
}()
}
}

59
tailwind.config.js Normal file
View File

@ -0,0 +1,59 @@
module.exports = {
future: {
removeDeprecatedGapUtilities: true,
purgeLayersByDefault: true,
},
purge: {
mode: 'all',
content: [
'./ui/**/*.jsx',
'./ui/**/*.html',
]
},
theme: {
extend: {
width: {
72: "18rem",
80: "20rem",
88: "22rem",
96: "24rem",
},
margin: {
72: "18rem",
80: "20rem",
88: "22rem",
96: "24rem",
}
},
colors: {
"gray": {
dark: "#313030",
medium: "#403F40",
light: "#8E8E8E"
},
"white": "#F9F9F9",
"dirty-white": "#ffe6c0",
"green": "#5EB663",
"green-light": "#92e897",
"blue": "#5C8FFF",
"blue-light": "#709DFF",
"red": "#FE5A5A",
"red-light": "#FF9B9B",
"orange": "#E39827",
"black": "#1C1C1C"
},
boxShadow: {
default: '0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06)',
md: '0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -1px rgba(0, 0, 0, .06)',
lg: '0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -2px rgba(0, 0, 0, .05)',
xl: '0 20px 25px -5px rgba(0, 0, 0, .1), 0 10px 10px -5px rgba(0, 0, 0, .04)',
"2xl": '0 25px 50px -12px rgba(0, 0, 0, .25)',
"3xl": '0 35px 60px -15px rgba(0, 0, 0, .3)',
inner: 'inset 0 4px 8px 0 rgba(0, 0, 0, 0.9)',
outline: '0 0 0 3px rgba(66, 153, 225, 0.5)',
'none': 'none',
}
},
variants: {},
plugins: [],
}

View File

@ -1,196 +1,87 @@
import React from 'react';
import {Switch, Route, withRouter} from 'react-router-dom';
import Header from './components/Header.jsx';
import Sidebar from './components/Sidebar.jsx';
import Footer from './components/Footer.jsx';
import Socket from '../socket.js';
import Index from "./components/Index";
import UsersContent from "./components/UsersContent";
import ModsContent from "./components/ModsContent";
import LogsContent from "./components/LogsContent";
import SavesContent from "./components/SavesContent";
import ConfigContent from "./components/ConfigContent";
import ConsoleContent from "./components/ConsoleContent";
import React, {useCallback, useState} from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.checkLogin = this.checkLogin.bind(this);
this.flashMessage = this.flashMessage.bind(this);
this.facServStatus = this.facServStatus.bind(this);
this.getSaves = this.getSaves.bind(this);
this.getStatus = this.getStatus.bind(this);
this.connectWebSocket = this.connectWebSocket.bind(this);
this.getFactorioVersion = this.getFactorioVersion.bind(this);
import user from "../api/resources/user";
import Login from "./views/Login";
import {Redirect, Route, Switch, useHistory} from "react-router";
import Controls from "./views/Controls";
import {BrowserRouter} from "react-router-dom";
import Logs from "./views/Logs";
import Saves from "./views/Saves/Saves";
import Layout from "./components/Layout";
import server from "../api/resources/server";
import Mods from "./views/Mods/Mods";
import UserManagement from "./views/UserManagement/UserManagment";
import ServerSettings from "./views/ServerSettings";
import GameSettings from "./views/GameSettings";
import Console from "./views/Console";
import Help from "./views/Help";
import socket from "../api/socket";
import {Flash} from "./components/Flash";
this.state = {
serverRunning: "stopped",
serverStatus: {},
factorioVersion: "",
saves: [],
loggedIn: false,
username: "",
messages: [],
showMessage: false,
const App = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [serverStatus, setServerStatus] = useState(null);
const history = useHistory();
const updateServerStatus = async () => {
const status = await server.status();
if (status) {
setServerStatus(status)
}
}
componentDidMount() {
this.checkLogin();
}
const handleAuthenticationStatus = useCallback(async () => {
const status = await user.status();
if (status?.Username) {
setIsAuthenticated(true);
await updateServerStatus();
connectWebSocket() {
let ws_scheme = window.location.protocol == "https:" ? "wss" : "ws";
let ws = new WebSocket(ws_scheme + "://" + window.location.host + "/ws");
this.socket = new Socket(ws);
}
flashMessage(message) {
var m = this.state.messages;
m.push(message);
this.setState({messages: m, showMessage: true});
}
checkLogin() {
$.ajax({
url: "/api/user/status",
type: "GET",
dataType: "json",
success: (data) => {
if (data.success === true) {
this.setState({
loggedIn: true,
username: data.data.Username
});
this.connectWebSocket();
this.getFactorioVersion(); //Init serverStatus, so i know, which factorio-version is installed
} else {
this.props.history.push("/login");
}
},
error: () => {
this.props.history.push("/login");
}
})
}
facServStatus() {
$.ajax({
url: "/api/server/status",
dataType: "json",
success: (data) => {
this.setState({
serverRunning: data.data.status
})
}
})
}
getSaves() {
$.ajax({
url: "/api/saves/list",
dataType: "json",
success: (data) => {
if (data.success === true) {
this.setState({saves: data.data})
} else {
this.setState({saves: []})
}
},
error: (xhr, status, err) => {
console.log('api/saves/list', status, err.toString());
}
});
if (!this.state.saves) {
this.setState({saves:[]});
socket.emit('server status subscribe');
socket.on('server_status', updateServerStatus)
}
}
},[]);
getStatus() {
$.ajax({
url: "/api/server/status",
dataType: "json",
success: (data) => {
this.setState({
serverStatus: data.data
})
},
error: (xhr, status, err) => {
console.log('api/server/status', status, err.toString());
}
})
}
getFactorioVersion() {
$.ajax({
url: "/api/server/facVersion",
// dataType: "json",
success: (data) => {
this.setState({
factorioVersion: data.data.base_mod_version
});
},
error: (xhr, status, err) => {
console.log('api/server/status', status, err.toString());
}
})
}
render() {
// render main application,
// if logged in show application
// if not logged in show Not logged in message
let appProps = {
message: "",
messages: this.state.messages,
flashMessage: this.flashMessage,
facServStatus: this.facServStatus,
serverStatus: this.state.serverStatus,
factorioVersion: this.state.factorioVersion,
getStatus: this.getStatus,
saves: this.state.saves,
getSaves: this.getSaves,
username: this.state.username,
socket: this.socket
};
let resp;
if (this.state.loggedIn) {
resp =
<div className="wrapper">
<Header
username={this.state.username}
loggedIn={this.state.loggedIn}
messages={this.state.messages}
/>
<Sidebar
serverStatus={this.facServStatus}
serverRunning={this.state.serverRunning}
/>
{/*Render react-router components and pass in props*/}
<Switch>
<Route path="/server" render={(props) => {return <Index {...props} {...appProps}/>}}/>
<Route path="/settings" render={(props) => {return <UsersContent {...props} {...appProps}/>}}/>
<Route path="/mods" render={(props) => {return <ModsContent {...props} {...appProps}/>}}/>
<Route path="/logs" render={(props) => {return <LogsContent {...props} {...appProps}/>}}/>
<Route path="/saves" render={(props) => {return <SavesContent {...props} {...appProps}/>}}/>
<Route path="/config" render={(props) => {return <ConfigContent {...props} {...appProps}/>}}/>
<Route path="/console" render={(props) => {return <ConsoleContent {...props} {...appProps}/>}}/>
<Route exact path="/" render={(props) => {return <Index {...props} {...appProps} />}}/>
</Switch>
<Footer />
</div>
} else {
resp = <div><p>Not Logged in</p></div>;
const handleLogout = useCallback(async () => {
const loggedOut = await user.logout();
if (loggedOut) {
setIsAuthenticated(false);
history.push('/login');
}
}, []);
return resp;
}
const ProtectedRoute = useCallback(({component: Component, ...rest}) => (
<Route {...rest} render={(props) => (
isAuthenticated && Component
? <Component serverStatus={serverStatus} updateServerStatus={updateServerStatus} {...props} />
: <Redirect to={{
pathname: '/login',
state: {from: props.location}
}}/>
)}/>
), [isAuthenticated, serverStatus]);
return (
<BrowserRouter basename="/">
<Switch>
<Route path="/login" render={() => (<Login handleLogin={handleAuthenticationStatus}/>)}/>
<Layout handleLogout={handleLogout} serverStatus={serverStatus} updateServerStatus={updateServerStatus}>
<ProtectedRoute exact path="/" component={Controls}/>
<ProtectedRoute path="/saves" component={Saves}/>
<ProtectedRoute path="/mods" component={Mods}/>
<ProtectedRoute path="/server-settings" component={ServerSettings}/>
<ProtectedRoute path="/game-settings" component={GameSettings}/>
<ProtectedRoute path="/console" component={Console}/>
<ProtectedRoute path="/logs" component={Logs}/>
<ProtectedRoute path="/user-management" component={UserManagement}/>
<ProtectedRoute path="/help" component={Help}/>
<Flash/>
</Layout>
</Switch>
</BrowserRouter>
);
}
export default withRouter(App);
export default App;

View File

@ -0,0 +1,37 @@
import React from "react";
import {faSpinner} from "@fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
const Button = ({ children, type, onClick, isSubmit, className, size, isLoading, isDisabled = false }) => {
let color = '';
let padding = '';
switch (type) {
case 'success':
color = `bg-green ${isDisabled || isLoading ? null : "hover:glow-green hover:bg-green-light" }`;
break;
case 'danger':
color = `bg-red ${isDisabled || isLoading ? null : "hover:glow-red hover:bg-red-light"}`;
break;
default:
color = `bg-gray-light ${isDisabled || isLoading ? null : "hover:glow-orange hover:bg-orange"}`
}
switch (size) {
case 'sm':
padding = 'py-1 px-2';
break;
default:
padding = 'py-2 px-4'
}
return (
<button onClick={onClick} disabled={isDisabled || isLoading} className={`${className ? className: null} ${isDisabled || isLoading ? "bg-opacity-50 cursor-not-allowed" : null} ${padding} ${color} inline-block accentuated text-black font-bold`}
type={isSubmit ? 'submit' : 'button'}>
{children} { isLoading && <FontAwesomeIcon icon={faSpinner} spin={true}/>}
</button>
);
}
export default Button;

View File

@ -0,0 +1,37 @@
import React from "react";
const ButtonLink = ({children, href, type, target, className, size}) => {
let color = '';
let padding = '';
switch (type) {
case 'success':
color = 'bg-green hover:glow-green hover:bg-green-light';
break;
case 'danger':
color = 'bg-red hover:glow-red hover:bg-red-light';
break;
default:
color = 'bg-gray-light hover:glow-orange hover:bg-orange'
}
switch (size) {
case 'sm':
padding = 'py-1 px-2';
break;
default:
padding = 'py-2 px-4'
}
return (
<a
href={href}
target={target ? target : '_self'}
className={`${className ? className : null} ${color} ${padding} inline-block accentuated text-black font-bold`}>
{children}
</a>
);
}
export default ButtonLink;

View File

@ -0,0 +1,17 @@
import React from "react";
const Checkbox = ({name, text, inputRef, checked}) => {
return (
<label className="block text-gray-500 font-bold">
<input
className="mr-2 leading-tight"
ref={inputRef}
name={name}
id={name}
type="checkbox" defaultChecked={checked}/>
<span className="text-sm">{text}</span>
</label>
)
}
export default Checkbox;

View File

@ -1,31 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class Settings extends React.Component {
constructor(props) {
super(props)
}
render() {
return(
<tbody>
{Object.keys(this.props.config).map(function(key) {
return(
<tr key={key}>
<td>{key}</td>
<td>{this.props.config[key]}</td>
</tr>
)
}, this)}
</tbody>
)
}
}
Settings.propTypes = {
section: PropTypes.string.isRequired,
config: PropTypes.object.isRequired,
}
export default Settings

View File

@ -1,362 +0,0 @@
import React from 'react';
import {Link} from 'react-router-dom';
import Settings from './Config/Settings.jsx';
import FontAwesomeIcon from "./FontAwesomeIcon";
//https://stackoverflow.com/a/1414175
function stringToBoolean(string) {
switch(string.toLowerCase().trim()) {
case "true":
case "yes":
case "1":
return true;
case "false":
case "no":
case "0":
case null:
return false;
default:
return Boolean(string);
}
}
class ConfigContent extends React.Component {
constructor(props) {
super(props);
this.getConfig = this.getConfig.bind(this);
this.getServerSettings = this.getServerSettings.bind(this);
this.updateServerSettings = this.updateServerSettings.bind(this);
this.handleServerSettingsChange = this.handleServerSettingsChange.bind(this);
this.formTypeField = this.formTypeField.bind(this);
this.capitalizeFirstLetter = this.capitalizeFirstLetter.bind(this)
this.state = {
config: {},
serverSettings: {}
}
}
componentDidMount() {
this.getConfig();
this.getServerSettings();
}
capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
handleServerSettingsChange(name, e) {
let fieldValue
var change = this.state.serverSettings;
// if true, false and something else can be used, dont switch to boolean type!
if(e.target.id == "allow_commands") {
fieldValue = e.target.value;
} else if(e.target.value === "true" || e.target.value === "false") {
// Ensure Boolean type is used if required
if(e.target.id === "lan" || e.target.id === "public") {
if(e.target.value == "true") {
fieldValue = true
} else {
fieldValue = false
}
change["visibility"][e.target.id] = fieldValue
this.setState({serverSettings: change});
return;
}
fieldValue = stringToBoolean(e.target.value)
} else if(e.target.id === "admins" || e.target.id === "tags") {
// Split settings values that are stored as arrays
fieldValue = e.target.value.split(",")
} else {
fieldValue = e.target.value
}
change[name] = fieldValue;
this.setState({serverSettings: change});
}
getConfig() {
$.ajax({
url: "/api/config",
dataType: "json",
success: (resp) => {
if(resp.success === true) {
this.setState({config: resp.data})
}
},
error: (xhr, status, err) => {
console.log('/api/config/get', status, err.toString());
}
});
}
getServerSettings() {
$.ajax({
url: "/api/settings",
dataType: "json",
success: (resp) => {
if(resp.success === true) {
this.setState({serverSettings: resp.data})
console.log(this.state)
}
},
error: (xhr, status, err) => {
console.log('/api/settings/get', status, err.toString());
}
});
}
updateServerSettings(e) {
e.preventDefault();
var serverSettingsJSON = JSON.stringify(this.state.serverSettings)
$.ajax({
url: "/api/settings/update",
datatype: "json",
type: "POST",
data: serverSettingsJSON,
success: (data) => {
console.log(data);
if(data.success === true) {
console.log("settings updated")
}
}
})
}
formTypeField(key, setting) {
if(key.startsWith("_comment_")) {
return (
<input
key={key}
ref={key}
id={key}
defaultValue={setting}
type="hidden"
/>
)
}
switch(typeof setting) {
case "number":
return (
<input
ref={key}
id={key}
className="form-control"
defaultValue={setting}
type="number"
onChange={this.handleServerSettingsChange.bind(this, key)}
/>
)
case "string":
if(key.includes("password")) {
return (
<input
ref={key}
id={key}
className="form-control"
defaultValue={setting}
type="password"
onChange={this.handleServerSettingsChange.bind(this, key)}
/>
)
} else {
return (
<input
ref={key}
id={key}
className="form-control"
defaultValue={setting}
type="text"
onChange={this.handleServerSettingsChange.bind(this, key)}
/>
)
}
case "boolean":
return (
<select
ref={key}
id={key}
className="form-control"
onChange={this.handleServerSettingsChange.bind(this, key)}
>
<option value={true}>True</option>
<option value={false}>False</option>
</select>
)
case "object":
if(Array.isArray(setting)) {
return (
<input
ref={key}
id={key}
className="form-control"
defaultValue={setting}
type="text"
onChange={this.handleServerSettingsChange.bind(this, key)}
/>
)
} else {
if(key.includes("visibility")) {
let vis_fields = []
for(const key in setting) {
const field =
<div key={key}>
<p>{key}</p>
<select
label={key}
ref={key}
id={key}
className="form-control"
onChange={this.handleServerSettingsChange.bind(this, key)}
value={setting[key]}
>
<option value={true}>True</option>
<option value={false}>False</option>
</select>
</div>
vis_fields.push(field)
}
return vis_fields
}
}
default:
return (
<input
ref={key}
id={key}
className="form-control"
defaultValue={setting}
type="text"
onChange={this.handleServerSettingsChange.bind(this, key)}
/>
)
}
}
render() {
return (
<div className="content-wrapper">
<section className="content-header">
<h1>
Config
<small>Manage game configuration</small>
<small className="float-sm-right">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/"><FontAwesomeIcon icon="tachometer-alt"/>Server Control</Link>
</li>
<li className="breadcrumb-item active">
<FontAwesomeIcon icon="cogs"/>Game configurations
</li>
</ol>
</small>
</h1>
</section>
<section className="content">
<div className="box">
<div className="box-header">
<h3 className="box-title">Server Settings</h3>
</div>
<div className="box-body">
<div className="row">
<div className="col-md-10">
<div className="server-settings-section">
<div className="table-responsive">
<form ref="settingsForm"
className="form-horizontal"
onSubmit={this.updateServerSettings}
>
{
Object.keys(this.state.serverSettings).map(function(key) {
if(key.startsWith("_comment_")) {
return (
<div key={key}>
{this.formTypeField(key, setting)}
</div>
);
}
var setting = this.state.serverSettings[key]
var setting_key = this.capitalizeFirstLetter(key.replace(/_/g, " "))
var comment = this.state.serverSettings["_comment_" + key]
return (
<div className="form-group" key={key}>
<label htmlFor={key}
className="control-label col-md-3"
>
{setting_key}
</label>
<div className="col-md-6">
{
this.formTypeField(key, setting)
}
<p className="help-block">{comment}</p>
</div>
</div>
)
}, this)
}
<div className="col-xs-6">
<div className="form-group">
<input className="form-control btn btn-success" type="submit"
ref="button" value="Update Settings"/>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="content">
<div className="box">
<div className="box-header">
<h3 className="box-title">Game Configuration</h3>
</div>
<div className="box-body">
<div className="row">
<div className="col-md-10">
{Object.keys(this.state.config).map(function(key) {
var conf = this.state.config[key]
return (
<div className="settings-section" key={key}>
<h3>{key}</h3>
<div className="table-responsive">
<table className="table table-striped">
<thead>
<tr>
<th>Setting name</th>
<th>Setting value</th>
</tr>
</thead>
<Settings
section={key}
config={conf}
/>
</table>
</div>
</div>
)
}, this)}
</div>
</div>
</div>
</div>
</section>
</div>
)
}
}
export default ConfigContent

View File

@ -0,0 +1,33 @@
import React, {useState} from 'react';
import Modal from "./Modal";
import Button from "./Button";
function ConfirmDialog({title, content, isOpen, close, onSuccess}) {
const [isLoading, setIsLoading] = useState(false);
const confirm = () => {
setIsLoading(true)
onSuccess()
.finally(() => {
close()
setIsLoading(false);
})
}
return (
<Modal
title={title}
content={content}
actions={
<>
<Button size="sm" type="danger" className="mr-2" onClick={close}>Cancel</Button>
<Button size="sm" isLoading={isLoading} type="success" onClick={confirm}>Confirm</Button>
</>
}
isOpen={isOpen}
/>
);
}
export default ConfirmDialog;

View File

@ -1,133 +0,0 @@
import React from 'react';
import {Link} from 'react-router-dom';
import PropTypes from 'prop-types';
import FontAwesomeIcon from "./FontAwesomeIcon";
class ConsoleContent extends React.Component {
constructor(props) {
super(props);
this.componentDidMount = this.componentDidMount.bind(this);
this.handleInput = this.handleInput.bind(this);
this.clearInput = this.clearInput.bind(this);
this.clearHistory = this.clearHistory.bind(this);
this.addHistory = this.addHistory.bind(this);
this.handleClick = this.handleClick.bind(this);
this.newLogLine = this.newLogLine.bind(this);
this.subscribeLogToSocket = this.subscribeLogToSocket.bind(this);
this.state = {
commands: {},
history: [],
prompt: '$ ',
}
}
componentDidMount() {
this.subscribeLogToSocket();
}
subscribeLogToSocket() {
let wsReadyState = this.props.socket.emit("log subscribe");
if(wsReadyState != WebSocket.OPEN) {
setTimeout(() => {
this.subscribeLogToSocket();
}, 50);
return;
}
this.setState({connected: true});
this.props.socket.on('log update', this.newLogLine.bind(this));
}
componentDidUpdate() {
var el = this.refs.output;
var container = document.getElementById("console-output");
container.scrollTop = this.refs.output.scrollHeight;
}
handleInput(e) {
if (e.key === "Enter") {
var input_text = this.refs.term.value;
this.props.socket.emit("command send", input_text);
this.addHistory(this.state.prompt + " " + input_text);
this.clearInput();
}
}
clearInput() {
this.refs.term.value = "";
}
clearHistory() {
ths.setState({ history: [] });
}
addHistory(output) {
var history = this.state.history;
history.push(output);
this.setState({
'history': history
});
}
handleClick() {
var term = this.refs.term;
term.focus();
}
newLogLine(logline) {
var history = this.state.history;
history.push(logline);
this.setState({
'history': history
});
}
render() {
var output = this.state.history.map((op, i) => {
return <p key={i}>{op}</p>
});
return(
<div className="content-wrapper">
<section className="content-header">
<h1>
Server Console
<small>Send commands and messages to the Factorio server</small>
<small className="float-sm-right">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/"><FontAwesomeIcon icon="tachometer-alt"/>Server Control</Link>
</li>
<li className="breadcrumb-item active">
<FontAwesomeIcon icon="terminal"/>Console
</li>
</ol>
</small>
</h1>
</section>
<section className="content">
<div className="console-box" >
<div id='console-output' className='console-container' onClick={this.handleClick} ref="output">
{output}
</div>
<p>
<span className="console-prompt-box">{this.state.prompt}
<input type="text" onKeyPress={this.handleInput} ref="term" /></span>
</p>
</div>
</section>
</div>
);
}
}
ConsoleContent.propTypes = {
socket: PropTypes.object.isRequired,
}
export default ConsoleContent;

View File

@ -0,0 +1,25 @@
import React, { useEffect, useState } from 'react';
import Bus from '../../notifications';
export const Flash = () => {
let [visibility, setVisibility] = useState(false);
let [message, setMessage] = useState('');
let [color, setColor] = useState('');
useEffect(() => {
Bus.addListener('flash', ({message, color}) => {
setVisibility(true);
setMessage(message);
setColor(color);
setTimeout(() => {
setVisibility(false);
}, 4000);
});
}, []);
return (
visibility && <div className={`bg-${color} accentuated rounded fixed bottom-0 right-0 mr-8 mb-8 px-4 py-2`}>
<p>{message}</p>
</div>
)
}

View File

@ -1,27 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
class FontAwesomeIcon extends React.Component {
constructor(props) {
super(props);
}
render() {
let classes = classNames(this.props.prefix, {
"fas": !this.props.prefix,
}, 'fa-' + this.props.icon, this.props.className);
return (
<i className={classes}></i>
);
}
}
FontAwesomeIcon.propTypes = {
icon: PropTypes.string.isRequired,
prefix: PropTypes.string,
className: PropTypes.string
};
export default FontAwesomeIcon;

View File

@ -1,15 +0,0 @@
import React from 'react';
class Footer extends React.Component {
render() {
return(
<footer className="main-footer">
<div className="pull-right hidden-xs">
</div>
<strong>Copyright &copy; 2019 <a href="https://github.com/MajorMJR/factorio-server-manager">Mitch Roote</a>.</strong> MIT License.
</footer>
)
}
}
export default Footer

View File

@ -1,66 +0,0 @@
import React from 'react';
import {Link, withRouter} from 'react-router-dom';
import PropTypes from 'prop-types';
import FontAwesomeIcon from "./FontAwesomeIcon";
class Header extends React.Component {
constructor(props) {
super(props);
this.onLogout = this.onLogout.bind(this);
}
onLogout(e) {
e.preventDefault();
$.ajax({
url: "/api/logout",
dataType: "json",
success: (resp) => {
console.log(resp)
}
});
// Wait for 1 second for logout callback to complete
setTimeout(() => {
this.props.history.push("/login")
}, 1000);
}
render() {
var loginMenu;
if (this.props.loggedIn) {
loginMenu =
<ul className="navbar-nav ml-auto">
<li className="nav-item">
<Link className="nav-link" to="/settings">
<FontAwesomeIcon icon="cogs" className="fa-fw"/>Settings
</Link>
</li>
<li className="nav-item">
<a href="javascript:void(0)" onClick={this.onLogout} className="nav-link">
<FontAwesomeIcon icon="lock" className="fa-fw"/>Logout
</a>
</li>
</ul>
}
return(
<nav className="main-header navbar navbar-expand navbar-light border-bottom">
<ul className="navbar-nav">
<li className="nav-item">
<a className="nav-link" data-widget="pushmenu" href="#">
<FontAwesomeIcon icon="bars"/>
</a>
</li>
</ul>
{loginMenu}
</nav>
)
}
}
Header.propTypes = {
username: PropTypes.string.isRequired,
loggedIn: PropTypes.bool.isRequired,
}
export default withRouter(Header);

View File

@ -1,59 +0,0 @@
import React from 'react';
import ServerCtl from './ServerCtl/ServerCtl.jsx';
import ServerStatus from './ServerCtl/ServerStatus.jsx';
import FontAwesomeIcon from "./FontAwesomeIcon";
class Index extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.facServStatus();
this.props.getSaves();
this.props.getStatus();
}
componentWillUnmount() {
this.props.facServStatus();
}
render() {
return(
<div className="content-wrapper" style={{height: "100%"}}>
<section className="content-header" style={{height: "100%"}}>
<h1>
Factorio Server
<small>Control your Factorio server</small>
<small className="float-sm-right">
<ol className="breadcrumb">
<li className="breadcrumb-item active">
<FontAwesomeIcon icon="tachometer-alt"/>Server Control
</li>
</ol>
</small>
</h1>
</section>
<section className="content">
<ServerStatus
serverStatus={this.props.serverStatus}
facServStatus={this.props.facServStatus}
getStatus={this.props.getStatus}
/>
<ServerCtl
getStatus={this.props.getStatus}
saves={this.props.saves}
getSaves={this.props.getSaves}
serverStatus={this.props.serverStatus}
facServStatus={this.props.facServStatus}
/>
</section>
</div>
)
}
}
export default Index

View File

@ -0,0 +1,19 @@
import React from "react";
const Input = ({name, inputRef, placeholder = null, type="text", defaultValue=null, hasAutoComplete=true, onKeyDown=() => null}) => {
return (
<input
className="shadow appearance-none border w-full py-2 px-3 text-black"
placeholder={placeholder}
ref={inputRef}
name={name}
id={name}
type={type}
onKeyDown={onKeyDown}
autoComplete={hasAutoComplete ? "on" : "off"}
defaultValue={defaultValue}
/>
)
}
export default Input;

View File

@ -0,0 +1,29 @@
import Input from "./Input";
import React, {useState} from "react";
import {faEye, faEyeSlash} from "@fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
const InputPassword = ({name, inputRef, defaultValue}) => {
const [type, setType] = useState("password");
let icon;
if (type === "password") {
icon = faEye;
} else {
icon = faEyeSlash
}
return (
<div className="flex">
<Input type={type} name={name} defaultValue={defaultValue} inputRef={inputRef} placeholder="*************"/>
<div
className="accentuated cursor-pointer bg-gray-light flex items-center px-2 text-black"
onClick={() => setType(type === "password" ? "text" : "password")}
>
<FontAwesomeIcon fixedWidth={true} icon={icon} />
</div>
</div>
)
}
export default InputPassword;

View File

@ -0,0 +1,12 @@
import React from "react";
const Label = ({text, htmlFor}) => {
return (
<label
className="block text-white text-sm font-bold mb-1" htmlFor={htmlFor}>
{text}
</label>
)
}
export default Label;

View File

@ -0,0 +1,108 @@
import React, {useEffect, useState} from "react";
import {NavLink} from "react-router-dom";
import Button from "./Button";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faBars} from "@fortawesome/free-solid-svg-icons";
const Layout = ({children, handleLogout, serverStatus, updateServerStatus}) => {
const [isNavCollapsed, setIsNavCollapsed] = useState(true);
useEffect(() => {
(async () => {
updateServerStatus()
})();
}, []);
const Status = ({info}) => {
let text = 'Unknown';
let color = 'gray-light';
if (info) {
if (info.status === 'running') {
text = 'Running';
color = 'green';
} else if (info.status === 'stopped') {
text = 'Stopped';
color = 'red';
}
}
return (
<div className={`bg-${color} accentuated rounded px-2 py-1 text-black`}>{text}</div>
)
}
const Link = ({children, to, last}) => {
return (
<NavLink
onClick={() => setIsNavCollapsed(true)}
exact={true}
to={to}
activeClassName="bg-orange"
className={`hover:glow-orange accentuated bg-gray-light hover:bg-orange text-black font-bold py-2 px-4 w-full block${last ? '' : ' mb-1'}`}
>{children}</NavLink>)
}
return (
<>
{/*Sidebar*/}
<div className="w-full md:w-88 md:fixed md:top-0 md:left-0 bg-gray-dark md:h-screen overflow-y-auto">
<div className="py-4 px-2 accentuated">
<div className="mx-4 justify-between flex text-center">
<span className="text-dirty-white text-xl">Factorio Server Manager</span>
<button
className="md:hidden cursor-pointer text-white hover:text-dirty-white"
onClick={() => setIsNavCollapsed(!isNavCollapsed)}
>
<FontAwesomeIcon icon={faBars}/>
</button>
</div>
</div>
<div className={isNavCollapsed ? "hidden md:block" : "block"}>
<div className="py-4 px-2 accentuated">
<h1 className="text-dirty-white text-lg mb-2 mx-4">Server Status</h1>
<div className="mx-4 mb-4 text-center">
<Status info={serverStatus}/>
</div>
</div>
<div className="py-4 px-2 accentuated">
<h1 className="text-dirty-white text-lg mb-2 mx-4">Server Management</h1>
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
<Link to="/">Controls</Link>
<Link to="/saves">Saves</Link>
<Link to="/mods">Mods</Link>
<Link to="/server-settings">Server Settings</Link>
<Link to="/game-settings">Game Settings</Link>
<Link to="/console">Console</Link>
<Link to="/logs" last={true}>Logs</Link>
</div>
</div>
<div className="py-4 px-2 accentuated">
<h1 className="text-dirty-white text-lg mb-2 mx-4">FSM Administration</h1>
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
<Link to="/user-management">Users</Link>
<Link to="/help" last={true}>Help</Link>
</div>
</div>
<div className="py-4 px-2 accentuated">
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
<Button type="danger" className="w-full" onClick={handleLogout}>Logout</Button>
</div>
</div>
<div className="accentuated-t accentuated-x md:block hidden"/>
</div>
</div>
{/*Main*/}
<div className="md:ml-88 min-h-screen">
<div className="container mx-auto pt-16 px-6">
{children}
</div>
</div>
</>
);
}
export default Layout;

View File

@ -1,79 +0,0 @@
import React from 'react';
import {withRouter} from 'react-router-dom';
import FontAwesomeIcon from "./FontAwesomeIcon";
class LoginContent extends React.Component {
constructor(props) {
super(props);
this.loginUser = this.loginUser.bind(this);
}
componentDidMount() {}
loginUser(e) {
e.preventDefault();
let user = {
username: this.refs.username.value,
password: this.refs.password.value,
};
$.ajax({
type: "POST",
url: "/api/login",
dataType: "json",
data: JSON.stringify(user),
success: (resp) => {
console.log(resp);
this.props.history.push("/");
}
});
}
render() {
return(
<div className="container" id="login">
<div className="d-flex justify-content-center h-100">
<div className="card">
<div className="card-header">
<h1>
<img src="./images/factorio.jpg" className="img-circle" alt="User Image"/>
Factorio Server Manager
</h1>
</div>
<div className="car-body">
<form onSubmit={this.loginUser}>
<label className="input-group form-group">
<div className="input-group-prepend">
<span className="input-group-text">
<FontAwesomeIcon icon="user"/>
</span>
</div>
<input className="form-control" type="text" ref="username" placeholder="Username"/>
</label>
<label className="input-group form-group">
<div className="input-group-prepend">
<span className="input-group-text">
<FontAwesomeIcon icon="lock"/>
</span>
</div>
<input className="form-control" type="password" ref="password" placeholder="Password"/>
</label>
<label className="remember-me">
<input type="checkbox"/>&nbsp;
Remember me
</label>
<input type="submit" value="Sign In" className="btn btn-primary btn-block btn-flat"/>
</form>
</div>
</div>
</div>
</div>
)
}
}
export default withRouter(LoginContent);

View File

@ -1,37 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class LogLines extends React.Component {
updateLog() {
this.props.getLastLog();
}
render() {
this.props.log.reverse();
return(
<div id="logLines" className="box">
<div className="box-header">
<h3 className="box-title">Factorio Log</h3>
</div>
<div className="box-body">
<input className="btn btn-default" type='button' onClick={this.updateLog.bind(this)} value="Refresh" />
<h5>Latest log line at the top</h5>
<samp>
{this.props.log.map ( (line, i) => {
return(
<p key={i}>{line}</p>
)
})}
</samp>
</div>
</div>
)
}
}
LogLines.propTypes = {
log: PropTypes.array.isRequired,
getLastLog: PropTypes.func.isRequired
}
export default LogLines

Some files were not shown because too many files have changed in this diff Show More