Reverse Proxy with SSL support, Generated client Configs, JWT client to server auth, closes #13
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -18,4 +18,6 @@ logs/server.log
|
|||||||
.goreleaser.yml
|
.goreleaser.yml
|
||||||
config.toml.backup
|
config.toml.backup
|
||||||
/public/static/js/kickwebsocket.js.backup
|
/public/static/js/kickwebsocket.js.backup
|
||||||
|
/public/static/js/kickwebsocket-generated.js
|
||||||
|
clientAuth.txt
|
||||||
dist
|
dist
|
@@ -39,6 +39,12 @@ Image of the frontend UI
|
|||||||
|
|
||||||
- [X] Add torrents from watch folder (cron job every 5 minutes)
|
- [X] Add torrents from watch folder (cron job every 5 minutes)
|
||||||
|
|
||||||
|
- [X] Authentication from client to server (done via JWT, will add functionality for 3rd party clients later)
|
||||||
|
|
||||||
|
- [X] Reverse Proxy Support with SSL upgrade added (with provided config for nginx)
|
||||||
|
|
||||||
|
- [X] Mostly generated client config from toml.config on first run
|
||||||
|
|
||||||
- [ ] Unit testing completed for a large portion of the package
|
- [ ] Unit testing completed for a large portion of the package
|
||||||
|
|
||||||
- [ ] Stability/bug fixing/Optimization rewrite of some of the core structures of the WebUI and base server
|
- [ ] Stability/bug fixing/Optimization rewrite of some of the core structures of the WebUI and base server
|
||||||
@@ -49,7 +55,6 @@ Image of the frontend UI
|
|||||||
|
|
||||||
- [ ] Ability to view TOML settings from WebUI (and perhaps change a few as well)
|
- [ ] Ability to view TOML settings from WebUI (and perhaps change a few as well)
|
||||||
|
|
||||||
- [ ] Authentication from client to server
|
|
||||||
|
|
||||||
- Late 2018
|
- Late 2018
|
||||||
|
|
||||||
|
26
config.toml
26
config.toml
@@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
[serverConfig]
|
[serverConfig]
|
||||||
|
|
||||||
ServerPort = ":8000" #leave format as is it expects a string with colon
|
ServerPort = ":8000" #leave format as is it expects a string with colon
|
||||||
ServerAddr = "" #blank will bind to default IP address, usually fine to leave be
|
ServerAddr = "" #blank will bind to default IP address, usually fine to leave be
|
||||||
LogLevel = "Warn" # Options = Debug, Info, Warn, Error, Fatal, Panic
|
LogLevel = "Debug" # Options = Debug, Info, Warn, Error, Fatal, Panic
|
||||||
LogOutput = "file" #Options = file, stdout #file will print it to logs/server.log
|
LogOutput = "stdout" #Options = file, stdout #file will print it to logs/server.log
|
||||||
|
|
||||||
SeedRatioStop = 1.50 #automatically stops the torrent after it reaches this seeding ratio
|
SeedRatioStop = 1.50 #automatically stops the torrent after it reaches this seeding ratio
|
||||||
|
|
||||||
#Relative or absolute path accepted, the server will convert any relative path to an absolute path.
|
#Relative or absolute path accepted, the server will convert any relative path to an absolute path.
|
||||||
DefaultMoveFolder = 'downloaded' #default path that a finished torrent is symlinked to after completion. Torrents added via RSS will default here
|
DefaultMoveFolder = 'downloaded' #default path that a finished torrent is symlinked to after completion. Torrents added via RSS will default here
|
||||||
TorrentWatchFolder = 'torrentUpload' #folder path that is watched for .torrent files and adds them automatically every 5 minutes
|
TorrentWatchFolder = 'torrentUpload' #folder path that is watched for .torrent files and adds them automatically every 5 minutes
|
||||||
@@ -17,18 +17,30 @@
|
|||||||
DownloadRateLimit = "Unlimited"
|
DownloadRateLimit = "Unlimited"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[notifications]
|
[notifications]
|
||||||
|
|
||||||
PushBulletToken = "" #add your pushbullet api token here to notify of torrent completion to pushbullet
|
PushBulletToken = "" #add your pushbullet api token here to notify of torrent completion to pushbullet
|
||||||
|
|
||||||
|
|
||||||
|
[goTorrentWebUI]
|
||||||
|
#Here you can set a basic HTTP login set of credentials
|
||||||
|
WebUIAuth = true # bool, if false no authentication is required for the webUI
|
||||||
|
WebUIUser = "admin"
|
||||||
|
WebUIPassword = "Password"
|
||||||
|
|
||||||
|
|
||||||
|
[reverseProxy]
|
||||||
|
#This is for setting up goTorrent behind a reverse Proxy (with SSL, reverse proxy with no SSL will require editing the WSS connection to a WS connection manually)
|
||||||
|
ProxyEnabled = false #bool, either false or true
|
||||||
|
BaseURL = "yoursubdomain.domain.org/subroute/" # MUST be in the format (if you have a subdomain, and must have trailing slash) "yoursubdomain.domain.org/subroute/"
|
||||||
|
|
||||||
[EncryptionPolicy]
|
[EncryptionPolicy]
|
||||||
|
|
||||||
DisableEncryption = false
|
DisableEncryption = false
|
||||||
ForceEncryption = false
|
ForceEncryption = false
|
||||||
PreferNoEncryption = true
|
PreferNoEncryption = true
|
||||||
|
|
||||||
|
|
||||||
[torrentClientConfig]
|
[torrentClientConfig]
|
||||||
DownloadDir = 'downloading' #the full OR relative path where the torrent server stores in-progress torrents
|
DownloadDir = 'downloading' #the full OR relative path where the torrent server stores in-progress torrents
|
||||||
|
|
||||||
@@ -37,6 +49,9 @@
|
|||||||
# Never send chunks to peers.
|
# Never send chunks to peers.
|
||||||
NoUpload = false #boolean
|
NoUpload = false #boolean
|
||||||
|
|
||||||
|
#User-provided Client peer ID. If not present, one is generated automatically.
|
||||||
|
PeerID = "" #string
|
||||||
|
|
||||||
#The address to listen for new uTP and TCP bittorrent protocol connections. DHT shares a UDP socket with uTP unless configured otherwise.
|
#The address to listen for new uTP and TCP bittorrent protocol connections. DHT shares a UDP socket with uTP unless configured otherwise.
|
||||||
ListenAddr = "" #Leave Blank for default, syntax "HOST:PORT"
|
ListenAddr = "" #Leave Blank for default, syntax "HOST:PORT"
|
||||||
|
|
||||||
@@ -48,9 +63,6 @@
|
|||||||
# Don't create a DHT.
|
# Don't create a DHT.
|
||||||
NoDHT = false #boolean
|
NoDHT = false #boolean
|
||||||
|
|
||||||
#User-provided Client peer ID. If not present, one is generated automatically.
|
|
||||||
PeerID = "" #string
|
|
||||||
|
|
||||||
#For the bittorrent protocol.
|
#For the bittorrent protocol.
|
||||||
DisableUTP = false #bool
|
DisableUTP = false #bool
|
||||||
|
|
||||||
|
12
dist-specific-files/Linux-systemd/ReverseProxy/nginx.conf
Normal file
12
dist-specific-files/Linux-systemd/ReverseProxy/nginx.conf
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
location ^~ /gotorrent/ {
|
||||||
|
proxy_pass http://192.168.1.100:8000/;
|
||||||
|
proxy_redirect http:// https://;
|
||||||
|
proxy_pass_header Server;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $http_address;
|
||||||
|
proxy_set_header X-Scheme $scheme;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "Upgrade";
|
||||||
|
|
||||||
|
}
|
60
engine/authentication_helper.go
Normal file
60
engine/authentication_helper.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthRequest struct {
|
||||||
|
MessageType string `json:"MessageType"`
|
||||||
|
AuthString string `json:"AuthString"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//GoTorrentClaims stores the name of the client (usually user entered) and any standard jwt claims we want to define
|
||||||
|
type GoTorrentClaims struct {
|
||||||
|
ClientName string `json:"clientName"`
|
||||||
|
jwt.StandardClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
//GenerateToken creates a signed token for a client to use to communicate with the server
|
||||||
|
func GenerateToken(claims GoTorrentClaims, signingKey []byte) string {
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
signedString, err := token.SignedString(signingKey)
|
||||||
|
if err != nil {
|
||||||
|
Logger.WithFields(logrus.Fields{"error": err}).Fatal("Error signing authentication Token!")
|
||||||
|
}
|
||||||
|
return signedString
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//GenerateSigningKey creates a random key that will be used for JSON Web Token authentication
|
||||||
|
func GenerateSigningKey() []byte {
|
||||||
|
keyString, err := generateRandomASCIIString(24)
|
||||||
|
key := []byte(keyString)
|
||||||
|
if err != nil {
|
||||||
|
Logger.WithFields(logrus.Fields{"error": err}).Fatal("Error generating signing key!")
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomASCIIString(length int) (string, error) {
|
||||||
|
result := ""
|
||||||
|
for {
|
||||||
|
if len(result) >= length {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(127)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
n := num.Int64()
|
||||||
|
// Make sure that the number/byte/letter is inside
|
||||||
|
// the range of printable ASCII characters (excluding space and DEL)
|
||||||
|
if n > 32 && n < 127 {
|
||||||
|
result += string(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
engine/clientConnectGenerate.go
Normal file
50
engine/clientConnectGenerate.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseFile = `
|
||||||
|
var authMessage = {
|
||||||
|
MessageType: "authRequest",
|
||||||
|
Payload: [ClientAuthString]
|
||||||
|
}
|
||||||
|
|
||||||
|
var kickStart = {
|
||||||
|
MessageType: "torrentListRequest"
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onopen = function()
|
||||||
|
{
|
||||||
|
ws.send(JSON.stringify(authMessage));
|
||||||
|
console.log("Sending authentication message...", JSON.stringify(authMessage))
|
||||||
|
ws.send(JSON.stringify(kickStart)); //sending out the first ping
|
||||||
|
console.log("Kicking off websocket to server.....", JSON.stringify(kickStart))
|
||||||
|
};`
|
||||||
|
)
|
||||||
|
|
||||||
|
//GenerateClientConfigFile runs at first run (no db client detected) to generate a js file for connecting
|
||||||
|
func GenerateClientConfigFile(config FullClientSettings, authString string) {
|
||||||
|
os.Remove("public/static/js/kickwebsocket-generated.js")
|
||||||
|
var clientFile string
|
||||||
|
if config.UseProxy {
|
||||||
|
clientFile = `
|
||||||
|
ClientAuthString = "` + authString + `"
|
||||||
|
|
||||||
|
var ws = new WebSocket("wss://` + config.BaseURL + `websocket")
|
||||||
|
` + baseFile
|
||||||
|
} else {
|
||||||
|
clientFile = `
|
||||||
|
IP = "` + config.HTTPAddrIP + `"
|
||||||
|
Port = "` + config.WebsocketClientPort + `"
|
||||||
|
ClientAuthString = "` + authString + `"
|
||||||
|
|
||||||
|
var ws = new WebSocket(` + "`" + `ws://${IP}:${Port}/websocket` + "`" + `); //creating websocket
|
||||||
|
` + baseFile
|
||||||
|
|
||||||
|
}
|
||||||
|
clientFileBytes := []byte(clientFile)
|
||||||
|
ioutil.WriteFile("public/static/js/kickwebsocket-generated.js", clientFileBytes, 0755)
|
||||||
|
}
|
@@ -41,7 +41,7 @@ type RSSFeedsNames struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TorrentList struct contains the torrent list that is sent to the client
|
//TorrentList struct contains the torrent list that is sent to the client
|
||||||
type TorrentList struct { //helps create the JSON structure that react expects to recieve
|
type TorrentList struct { //helps create the JSON structure that react expects to receive
|
||||||
MessageType string `json:"MessageType"`
|
MessageType string `json:"MessageType"`
|
||||||
Totaltorrents int `json:"total"`
|
Totaltorrents int `json:"total"`
|
||||||
ClientDBstruct []ClientDB `json:"data"`
|
ClientDBstruct []ClientDB `json:"data"`
|
||||||
|
@@ -3,6 +3,7 @@ package engine
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
@@ -17,6 +18,10 @@ type FullClientSettings struct {
|
|||||||
LoggingLevel logrus.Level
|
LoggingLevel logrus.Level
|
||||||
LoggingOutput string
|
LoggingOutput string
|
||||||
HTTPAddr string
|
HTTPAddr string
|
||||||
|
HTTPAddrIP string
|
||||||
|
UseProxy bool
|
||||||
|
WebsocketClientPort string
|
||||||
|
BaseURL string
|
||||||
Version int
|
Version int
|
||||||
TorrentConfig torrent.Config
|
TorrentConfig torrent.Config
|
||||||
TFileUploadFolder string
|
TFileUploadFolder string
|
||||||
@@ -98,9 +103,17 @@ func FullClientSettingsNew() FullClientSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var httpAddr string
|
var httpAddr string
|
||||||
|
var baseURL string
|
||||||
|
var websocketClientPort string
|
||||||
|
|
||||||
httpAddrIP := viper.GetString("serverConfig.ServerAddr")
|
httpAddrIP := viper.GetString("serverConfig.ServerAddr")
|
||||||
httpAddrPort := viper.GetString("serverConfig.ServerPort")
|
httpAddrPort := viper.GetString("serverConfig.ServerPort")
|
||||||
|
proxySet := viper.GetBool("reverseProxy.ProxyEnabled")
|
||||||
|
websocketClientPort = strings.TrimLeft(viper.GetString("serverConfig.ServerPort"), ":") //Trimming off the colon in front of the port
|
||||||
|
if proxySet {
|
||||||
|
baseURL = viper.GetString("reverseProxy.BaseURL")
|
||||||
|
fmt.Println("WebsocketClientPort", viper.GetString("serverConfig.ServerPort"))
|
||||||
|
}
|
||||||
seedRatioStop := viper.GetFloat64("serverConfig.SeedRatioStop")
|
seedRatioStop := viper.GetFloat64("serverConfig.SeedRatioStop")
|
||||||
httpAddr = httpAddrIP + httpAddrPort
|
httpAddr = httpAddrIP + httpAddrPort
|
||||||
pushBulletToken := viper.GetString("notifications.PushBulletToken")
|
pushBulletToken := viper.GetString("notifications.PushBulletToken")
|
||||||
@@ -196,7 +209,11 @@ func FullClientSettingsNew() FullClientSettings {
|
|||||||
LoggingOutput: logOutput,
|
LoggingOutput: logOutput,
|
||||||
SeedRatioStop: seedRatioStop,
|
SeedRatioStop: seedRatioStop,
|
||||||
HTTPAddr: httpAddr,
|
HTTPAddr: httpAddr,
|
||||||
|
HTTPAddrIP: httpAddrIP,
|
||||||
|
UseProxy: proxySet,
|
||||||
|
WebsocketClientPort: websocketClientPort,
|
||||||
TorrentConfig: tConfig,
|
TorrentConfig: tConfig,
|
||||||
|
BaseURL: baseURL,
|
||||||
TFileUploadFolder: "uploadedTorrents",
|
TFileUploadFolder: "uploadedTorrents",
|
||||||
PushBulletToken: pushBulletToken,
|
PushBulletToken: pushBulletToken,
|
||||||
DefaultMoveFolder: defaultMoveFolderAbs,
|
DefaultMoveFolder: defaultMoveFolderAbs,
|
||||||
|
@@ -18,6 +18,8 @@ import LeftMenu from './leftMenu/leftMenu';
|
|||||||
import TorrentList from './torrentlist';
|
import TorrentList from './torrentlist';
|
||||||
//Notification Element
|
//Notification Element
|
||||||
import Notifications from './notifications';
|
import Notifications from './notifications';
|
||||||
|
//Login Box
|
||||||
|
import Login from './login';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ class BasicLayout extends React.PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return [
|
return [
|
||||||
|
<Login />,
|
||||||
<Notifications />,
|
<Notifications />,
|
||||||
<ReactGridLayout layout={this.state.layout} onLayoutChange={this.onLayoutChange}
|
<ReactGridLayout layout={this.state.layout} onLayoutChange={this.onLayoutChange}
|
||||||
{...this.props}>
|
{...this.props}>
|
||||||
|
113
goTorrentWebUI/src/login.js
Normal file
113
goTorrentWebUI/src/login.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import Button from 'material-ui/Button';
|
||||||
|
import TextField from 'material-ui/TextField';
|
||||||
|
import { withStyles } from 'material-ui/styles';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Dialog, {
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
} from 'material-ui/Dialog';
|
||||||
|
import InsertLinkIcon from 'material-ui-icons/Link';
|
||||||
|
import ReactTooltip from 'react-tooltip'
|
||||||
|
import Icon from 'material-ui/Icon';
|
||||||
|
import IconButton from 'material-ui/IconButton';
|
||||||
|
|
||||||
|
|
||||||
|
let Loggedin = false
|
||||||
|
|
||||||
|
const button = {
|
||||||
|
fontSize: '60px',
|
||||||
|
marginRight: '20px',
|
||||||
|
}
|
||||||
|
|
||||||
|
const inlineStyle = {
|
||||||
|
display: 'inline-block',
|
||||||
|
backdrop: 'static',
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorStyle = {
|
||||||
|
color: 'red',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Login extends React.Component {
|
||||||
|
|
||||||
|
state = {
|
||||||
|
open: false,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
wrongPasswordMessage: "",
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
componentWillMount = () => {
|
||||||
|
if ((LoginRequired) && (Loggedin == false)) {
|
||||||
|
this.setState({open: true})
|
||||||
|
Loggedin = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = () => {
|
||||||
|
//this.setState({ open: false });
|
||||||
|
//let magnetLinkSubmit = this.state.textValue;
|
||||||
|
console.log("Attempting authentication")
|
||||||
|
if ((this.state.username == ClientUsername) && (this.state.password == ClientPassword)) {
|
||||||
|
this.setState({ open: false, username: "", password: "" });
|
||||||
|
} else {
|
||||||
|
this.setState({wrongPasswordMessage: "Wrong Username/Password!", username: "", password: "" })
|
||||||
|
}
|
||||||
|
//this.setState({magnetLinkValue: ""}, {torrentLabel: ""}, {storageValue: ``})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRequestClose = () => {
|
||||||
|
ws.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserNameValue = (event) => {
|
||||||
|
this.setState({username: event.target.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
setPasswordValue = (event) => {
|
||||||
|
this.setState({password: event.target.value})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes, onRequestClose, handleRequestClose, handleSubmit } = this.props;
|
||||||
|
return (
|
||||||
|
<Dialog open={this.state.open} onRequestClose={this.handleRequestClose} disableBackdropClick={true} disableEscapeKeyDown={true} hideBackdrop={true} fullScreen>
|
||||||
|
<DialogTitle>Login Here</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
Enter a username and password to connect
|
||||||
|
</DialogContentText>
|
||||||
|
<br />
|
||||||
|
<DialogContentText style={errorStyle}>
|
||||||
|
{this.state.wrongPasswordMessage}
|
||||||
|
</DialogContentText>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
id="name"
|
||||||
|
label="User Name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
fullWidth
|
||||||
|
onChange={this.setUserNameValue}
|
||||||
|
/>
|
||||||
|
<TextField id="password" type="password" label="Password" placeholder="Password" fullWidth onChange={this.setPasswordValue} />
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={this.handleRequestClose} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={this.handleSubmit} color="primary">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
93
main.go
93
main.go
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -18,6 +17,8 @@ import (
|
|||||||
"github.com/asdine/storm"
|
"github.com/asdine/storm"
|
||||||
Engine "github.com/deranjer/goTorrent/engine"
|
Engine "github.com/deranjer/goTorrent/engine"
|
||||||
Storage "github.com/deranjer/goTorrent/storage"
|
Storage "github.com/deranjer/goTorrent/storage"
|
||||||
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/mmcdole/gofeed"
|
"github.com/mmcdole/gofeed"
|
||||||
@@ -36,6 +37,7 @@ type SingleRSSFeedMessage struct { //TODO had issues with getting this to work w
|
|||||||
var (
|
var (
|
||||||
//Logger does logging for the entire project
|
//Logger does logging for the entire project
|
||||||
Logger = logrus.New()
|
Logger = logrus.New()
|
||||||
|
Authenticated = false //to verify if user is authenticated, this is stored here
|
||||||
APP_ID = os.Getenv("APP_ID")
|
APP_ID = os.Getenv("APP_ID")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,6 +51,34 @@ func serveHome(w http.ResponseWriter, r *http.Request) {
|
|||||||
s1.ExecuteTemplate(w, "base", map[string]string{"APP_ID": APP_ID})
|
s1.ExecuteTemplate(w, "base", map[string]string{"APP_ID": APP_ID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleAuthentication(conn *websocket.Conn, db *storm.DB) {
|
||||||
|
msg := Engine.Message{}
|
||||||
|
err := conn.ReadJSON(&msg)
|
||||||
|
if err != nil {
|
||||||
|
Logger.WithFields(logrus.Fields{"error": err, "SuppliedToken": msg.Payload[0]}).Error("Unable to read authentication message")
|
||||||
|
}
|
||||||
|
authString := msg.Payload[0] //First element will be the auth request
|
||||||
|
fmt.Println("Authstring", authString)
|
||||||
|
signingKeyStruct := Storage.FetchJWTTokens(db)
|
||||||
|
singingKey := signingKeyStruct.SigningKey
|
||||||
|
token, err := jwt.Parse(authString, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return singingKey, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
Logger.WithFields(logrus.Fields{"error": err, "SuppliedToken": token}).Error("Unable to parse token!")
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||||
|
fmt.Println("Claims", claims["ClientName"], claims["Issuer"])
|
||||||
|
Authenticated = true
|
||||||
|
} else {
|
||||||
|
Logger.WithFields(logrus.Fields{"error": err}).Error("Authentication Error occured, cannot complete!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
Engine.Logger = Logger //Injecting the logger into all the packages
|
Engine.Logger = Logger //Injecting the logger into all the packages
|
||||||
Storage.Logger = Logger
|
Storage.Logger = Logger
|
||||||
@@ -92,6 +122,34 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer db.Close() //defering closing the database until the program closes
|
defer db.Close() //defering closing the database until the program closes
|
||||||
|
|
||||||
|
tokens := Storage.IssuedTokensList{} //if first run setting up the authentication tokens
|
||||||
|
err = db.One("ID", 3, &tokens)
|
||||||
|
if err != nil {
|
||||||
|
Logger.WithFields(logrus.Fields{"RSSFeedStore": tokens, "error": err}).Info("No Tokens database found, assuming first run, generating token...")
|
||||||
|
fmt.Println("Error", err)
|
||||||
|
fmt.Println("MAIN TOKEN: %+v\n", tokens)
|
||||||
|
tokens.ID = 3 //creating the initial store
|
||||||
|
claims := Engine.GoTorrentClaims{
|
||||||
|
"goTorrentWebUI",
|
||||||
|
jwt.StandardClaims{
|
||||||
|
Issuer: "goTorrentServer",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
signingkey := Engine.GenerateSigningKey() //Running this will invalidate any certs you already issued!!
|
||||||
|
fmt.Println("SigningKey", signingkey)
|
||||||
|
authString := Engine.GenerateToken(claims, signingkey)
|
||||||
|
tokens.SigningKey = signingkey
|
||||||
|
fmt.Println("ClientToken: ", authString)
|
||||||
|
Engine.GenerateClientConfigFile(Config, authString) //if first run generate the client config file
|
||||||
|
|
||||||
|
tokens.TokenNames = append(tokens.TokenNames, Storage.SingleToken{"firstClient"})
|
||||||
|
err := ioutil.WriteFile("clientAuth.txt", []byte(authString), 0755)
|
||||||
|
if err != nil {
|
||||||
|
Logger.WithFields(logrus.Fields{"error": err}).Warn("Unable to write client auth to file..")
|
||||||
|
}
|
||||||
|
db.Save(&tokens) //Writing all of that to the database
|
||||||
|
}
|
||||||
|
|
||||||
cronEngine := Engine.InitializeCronEngine() //Starting the cron engine for tasks
|
cronEngine := Engine.InitializeCronEngine() //Starting the cron engine for tasks
|
||||||
Logger.Debug("Cron Engine Initialized...")
|
Logger.Debug("Cron Engine Initialized...")
|
||||||
|
|
||||||
@@ -113,10 +171,12 @@ func main() {
|
|||||||
Engine.RefreshRSSCron(cronEngine, db, tclient, torrentLocalStorage, Config) // Refresing the RSS feeds on an hourly basis to add torrents that show up in the RSS feed
|
Engine.RefreshRSSCron(cronEngine, db, tclient, torrentLocalStorage, Config) // Refresing the RSS feeds on an hourly basis to add torrents that show up in the RSS feed
|
||||||
|
|
||||||
router := mux.NewRouter() //setting up the handler for the web backend
|
router := mux.NewRouter() //setting up the handler for the web backend
|
||||||
|
//reverseProxy := handlers.ProxyHeaders(router) //handlers.ProxyHeaders(router) //TODO pull this from the config file
|
||||||
router.HandleFunc("/", serveHome) //Serving the main page for our SPA
|
router.HandleFunc("/", serveHome) //Serving the main page for our SPA
|
||||||
http.Handle("/static/", http.FileServer(http.Dir("public")))
|
//http.Handle("/static/", http.FileServer(http.Dir("public")))
|
||||||
|
router.PathPrefix("/static/").Handler(http.FileServer(http.Dir("public")))
|
||||||
http.Handle("/", router)
|
http.Handle("/", router)
|
||||||
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) { //exposing the data to the
|
router.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) { //exposing the data to the
|
||||||
TorrentLocalArray = Storage.FetchAllStoredTorrents(db)
|
TorrentLocalArray = Storage.FetchAllStoredTorrents(db)
|
||||||
RunningTorrentArray = Engine.CreateRunningTorrentArray(tclient, TorrentLocalArray, PreviousTorrentArray, Config, db) //Updates the RunningTorrentArray with the current client data as well
|
RunningTorrentArray = Engine.CreateRunningTorrentArray(tclient, TorrentLocalArray, PreviousTorrentArray, Config, db) //Updates the RunningTorrentArray with the current client data as well
|
||||||
var torrentlistArray = new(Engine.TorrentList)
|
var torrentlistArray = new(Engine.TorrentList)
|
||||||
@@ -127,15 +187,21 @@ func main() {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write(torrentlistArrayJSON)
|
w.Write(torrentlistArrayJSON)
|
||||||
})
|
})
|
||||||
http.HandleFunc("/websocket", func(w http.ResponseWriter, r *http.Request) { //websocket is the main data pipe to the frontend
|
|
||||||
|
router.HandleFunc("/websocket", func(w http.ResponseWriter, r *http.Request) { //websocket is the main data pipe to the frontend
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
defer conn.Close() //defer closing the websocket until done.
|
defer conn.Close() //defer closing the websocket until done.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.WithFields(logrus.Fields{"error": err}).Fatal("Unable to create websocket!")
|
Logger.WithFields(logrus.Fields{"error": err}).Fatal("Unable to create websocket!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Engine.Conn = conn //Injecting the conn variable into the other packages
|
if Authenticated != true {
|
||||||
|
handleAuthentication(conn, db)
|
||||||
|
} else { //If we are authenticated inject the connection into the other packages
|
||||||
|
fmt.Println("Authenticated... continue")
|
||||||
|
Engine.Conn = conn
|
||||||
Storage.Conn = conn
|
Storage.Conn = conn
|
||||||
|
}
|
||||||
MessageLoop: //Tagging this so we can continue out of it with any errors we encounter that are failing
|
MessageLoop: //Tagging this so we can continue out of it with any errors we encounter that are failing
|
||||||
for {
|
for {
|
||||||
runningTorrents := tclient.Torrents() //getting running torrents here since multiple cases ask for the running torrents
|
runningTorrents := tclient.Torrents() //getting running torrents here since multiple cases ask for the running torrents
|
||||||
@@ -148,6 +214,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
Logger.WithFields(logrus.Fields{"message": msg}).Debug("Message From Client")
|
Logger.WithFields(logrus.Fields{"message": msg}).Debug("Message From Client")
|
||||||
switch msg.MessageType { //first handling data requests
|
switch msg.MessageType { //first handling data requests
|
||||||
|
case "authRequest":
|
||||||
|
if Authenticated {
|
||||||
|
Logger.WithFields(logrus.Fields{"message": msg}).Debug("Client already authenticated... skipping authentication method")
|
||||||
|
} else {
|
||||||
|
handleAuthentication(conn, db)
|
||||||
|
}
|
||||||
|
|
||||||
case "torrentListRequest":
|
case "torrentListRequest":
|
||||||
Logger.WithFields(logrus.Fields{"message": msg}).Debug("Client Requested TorrentList Update")
|
Logger.WithFields(logrus.Fields{"message": msg}).Debug("Client Requested TorrentList Update")
|
||||||
TorrentLocalArray = Storage.FetchAllStoredTorrents(db) //Required to re-read th database since we write to the DB and this will pull the changes from it
|
TorrentLocalArray = Storage.FetchAllStoredTorrents(db) //Required to re-read th database since we write to the DB and this will pull the changes from it
|
||||||
@@ -491,9 +564,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
if err := http.ListenAndServe(httpAddr, nil); err != nil {
|
if Config.UseProxy {
|
||||||
|
err := http.ListenAndServe(httpAddr, handlers.ProxyHeaders(router))
|
||||||
|
if err != nil {
|
||||||
Logger.WithFields(logrus.Fields{"error": err}).Fatal("Unable to listen on the http Server!")
|
Logger.WithFields(logrus.Fields{"error": err}).Fatal("Unable to listen on the http Server!")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Server started on:", httpAddr)
|
err := http.ListenAndServe(httpAddr, nil) //Can't send proxy headers if not used since that can be a security issue
|
||||||
|
if err != nil {
|
||||||
|
Logger.WithFields(logrus.Fields{"error": err}).Fatal("Unable to listen on the http Server with no proxy headers!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
26
public/static/js/kickwebsocket-manual.js
Normal file
26
public/static/js/kickwebsocket-manual.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//This is the basic template used to generate the kickwebsocket. If needed you can manually edit this one to your needs and replace kickwebsocket-generated.
|
||||||
|
|
||||||
|
IP = "192.168.1.100"
|
||||||
|
Port = "8000"
|
||||||
|
ClientAuthString = "" //String generated on first start and stored in the root as "clientAuth.txt"
|
||||||
|
|
||||||
|
//var ws = new WebSocket(`ws://${IP}:${Port}/websocket`); //for websockets not over an SSL reverse proxy
|
||||||
|
|
||||||
|
//var ws = new WebSocket("wss://yoursubdomain.domain.org/subroute/") //for websockets behind an SSL reverse proxy
|
||||||
|
|
||||||
|
var authMessage = {
|
||||||
|
MessageType: "authRequest",
|
||||||
|
Payload: [ClientAuthString]
|
||||||
|
}
|
||||||
|
|
||||||
|
var kickStart = {
|
||||||
|
MessageType: "torrentListRequest"
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onopen = function()
|
||||||
|
{
|
||||||
|
ws.send(JSON.stringify(authMessage));
|
||||||
|
console.log("Sending authentication message...", JSON.stringify(authMessage))
|
||||||
|
ws.send(JSON.stringify(kickStart)); //sending out the first ping
|
||||||
|
console.log("Kicking off websocket to server.....", JSON.stringify(kickStart))
|
||||||
|
};
|
@@ -1,12 +0,0 @@
|
|||||||
var ws = new WebSocket("ws://192.168.1.141:8000/websocket"); //creating websocket
|
|
||||||
|
|
||||||
var kickStart = {
|
|
||||||
MessageType: "torrentListRequest"
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.onopen = function()
|
|
||||||
{
|
|
||||||
|
|
||||||
ws.send(JSON.stringify(kickStart)); //sending out the first ping
|
|
||||||
console.log("Kicking off websocket to server.....", JSON.stringify(kickStart))
|
|
||||||
};
|
|
BIN
storage.db.old
Normal file
BIN
storage.db.old
Normal file
Binary file not shown.
@@ -15,6 +15,24 @@ var Logger *logrus.Logger
|
|||||||
//Conn is the global websocket connection used to push server notification messages
|
//Conn is the global websocket connection used to push server notification messages
|
||||||
var Conn *websocket.Conn
|
var Conn *websocket.Conn
|
||||||
|
|
||||||
|
//IssuedTokensList contains a slice of all the tokens issues to applications
|
||||||
|
type IssuedTokensList struct {
|
||||||
|
ID int `storm:"id,unique"` //storm requires unique ID (will be 3) to save although there will only be one of these
|
||||||
|
SigningKey []byte
|
||||||
|
TokenNames []SingleToken
|
||||||
|
}
|
||||||
|
|
||||||
|
//SingleToken stores a single token and all of the associated information
|
||||||
|
type SingleToken struct {
|
||||||
|
ClientName string
|
||||||
|
}
|
||||||
|
|
||||||
|
//TorrentHistoryList holds the entire history of downloaded torrents by hash TODO implement a way to read this and maybe grab the name for every torrent as well
|
||||||
|
type TorrentHistoryList struct {
|
||||||
|
ID int `storm:"id,unique"` //storm requires unique ID (will be 2) to save although there will only be one of these
|
||||||
|
HashList []string
|
||||||
|
}
|
||||||
|
|
||||||
//RSSFeedStore stores all of our RSS feeds in a slice of gofeed.Feed
|
//RSSFeedStore stores all of our RSS feeds in a slice of gofeed.Feed
|
||||||
type RSSFeedStore struct {
|
type RSSFeedStore struct {
|
||||||
ID int `storm:"id,unique"` //storm requires unique ID (will be 1) to save although there will only be one of these
|
ID int `storm:"id,unique"` //storm requires unique ID (will be 1) to save although there will only be one of these
|
||||||
@@ -64,12 +82,6 @@ type TorrentLocal struct {
|
|||||||
TorrentFilePriority []TorrentFilePriority
|
TorrentFilePriority []TorrentFilePriority
|
||||||
}
|
}
|
||||||
|
|
||||||
//TorrentHistoryList holds the entire history of downloaded torrents by hash TODO implement a way to read this and maybe grab the name for every torrent as well
|
|
||||||
type TorrentHistoryList struct {
|
|
||||||
ID int `storm:"id,unique"` //storm requires unique ID (will be 2) to save although there will only be one of these
|
|
||||||
HashList []string
|
|
||||||
}
|
|
||||||
|
|
||||||
//FetchAllStoredTorrents is called to read in ALL local stored torrents in the boltdb database (called on server restart)
|
//FetchAllStoredTorrents is called to read in ALL local stored torrents in the boltdb database (called on server restart)
|
||||||
func FetchAllStoredTorrents(torrentStorage *storm.DB) (torrentLocalArray []*TorrentLocal) {
|
func FetchAllStoredTorrents(torrentStorage *storm.DB) (torrentLocalArray []*TorrentLocal) {
|
||||||
torrentLocalArray = []*TorrentLocal{} //creating the array of the torrentlocal struct
|
torrentLocalArray = []*TorrentLocal{} //creating the array of the torrentlocal struct
|
||||||
@@ -188,6 +200,25 @@ func StoreHashHistory(db *storm.DB, torrentHash string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//FetchJWTTokens fetches the stored client authentication tokens
|
||||||
|
func FetchJWTTokens(db *storm.DB) IssuedTokensList {
|
||||||
|
tokens := IssuedTokensList{}
|
||||||
|
err := db.One("ID", 3, &tokens)
|
||||||
|
if err != nil {
|
||||||
|
Logger.WithFields(logrus.Fields{"Tokens": tokens, "error": err}).Error("Unable to fetch Token database... should always be one token in database")
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
//UpdateJWTTokens updates the database with new tokens as they are added
|
||||||
|
func UpdateJWTTokens(db *storm.DB, tokens IssuedTokensList) {
|
||||||
|
err := db.Update(&tokens)
|
||||||
|
if err != nil {
|
||||||
|
Logger.WithFields(logrus.Fields{"Tokens": tokens, "error": err}).Error("Unable to update Token database")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
//FetchRSSFeeds fetches the RSS feed from db, which was setup when initializing database on first startup
|
//FetchRSSFeeds fetches the RSS feed from db, which was setup when initializing database on first startup
|
||||||
func FetchRSSFeeds(db *storm.DB) RSSFeedStore {
|
func FetchRSSFeeds(db *storm.DB) RSSFeedStore {
|
||||||
RSSFeed := RSSFeedStore{}
|
RSSFeed := RSSFeedStore{}
|
||||||
|
@@ -3,13 +3,13 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>goTorrent</title>
|
<title>goTorrent</title>
|
||||||
<link rel="icon" href="/static/favicon/goTorrentFavicon.ico">
|
<link rel="icon" href="static/favicon/goTorrentFavicon.ico">
|
||||||
<script type="text/javascript" src="/static/js/kickwebsocket.js"></script>
|
<script type="text/javascript" src="static/js/kickwebsocket-generated.js"></script>
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id=app></div>
|
<div id=app></div>
|
||||||
<script type="text/javascript" src="/static/js/bundle.js"></script>
|
<script type="text/javascript" src="static/js/bundle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
Reference in New Issue
Block a user