cleanup (tunnel): deprecate step 2 of the setup

This commit is contained in:
Mickael Kerjean 2020-12-22 19:24:25 +11:00
parent 46cde8a166
commit f15cd1959b
4 changed files with 14 additions and 341 deletions

View file

@ -67,31 +67,6 @@ export class SetupPage extends React.Component {
});
}
onExposeInstance(choice, done){
this.setState({busy: true});
return Config.all().then((config) => {
config = FormObjToJSON(config);
config.connections = window.CONFIG.connections;
switch(choice){
case "tunnel":
config.features.server.enable_tunnel = true;
break;
default:
config.features.server.enable_tunnel = false;
break;
}
Config.save(config, false)
.then(() => this.setState({busy: false}, done))
.catch((err) => {
notify.send(err && err.message, "error");
this.setState({busy: false});
});
}).catch((err) => {
notify.send(err && err.message, "error");
this.setState({busy: false});
});
}
enableLog(value){
Config.all().then((config) => {
config = FormObjToJSON(config);
@ -111,7 +86,7 @@ export class SetupPage extends React.Component {
"name_failure": "SSL is not configured properly",
"pass": window.location.protocol !== "http:",
"severe": true,
"message": "This can lead to data leaks. Please use a SSL certificate or expose your instance via a filestash domain"
"message": "This can lead to data leaks. Please use a SSL certificate"
}, {
"name_success": "Application is running as '" + objectGet(config, ["constant", "user", "value"]) + "'",
"name_failure": "Application is running as root",
@ -137,22 +112,13 @@ export class SetupPage extends React.Component {
this.setState({busy: false});
});
}
tunnelCall(){
this.setState({busy: true});
return Config.all().then((config) => {
//this.setState({busy: false});
return objectGet(config, ["features", "server", "tunnel_url", "value"]);
});
}
render(){
return (
<div className="component_setup">
<MultiStepForm loading={this.state.busy}
onAdminPassword={this.onAdminPassword.bind(this)}
onExposeInstance={this.onExposeInstance.bind(this) }
summaryCall={this.summaryCall.bind(this)}
tunnelCall={this.tunnelCall.bind(this)} />
summaryCall={this.summaryCall.bind(this)} />
</div>
);
}
@ -166,73 +132,29 @@ class MultiStepForm extends React.Component {
current: parseInt(window.location.hash.replace("#", "")) || 0,
answer_password: "",
has_answered_password: false,
answer_expose: "",
has_answered_expose: false,
deps: [],
redirect_uri: null,
working_message: "Working"
deps: []
};
}
componentDidMount(){
if(this.state.current == 2){
this.fetchDependencies();
if(this.state.current === 1){
this.props.summaryCall().then((deps) => {
this.setState({deps: deps});
});
}
}
onAdminPassword(e){
e.preventDefault();
this.props.onAdminPassword(this.state.answer_password, () => {
this.setState({current: 1, has_answered_password: true});
});
}
onExposeInstance(value, e){
e.preventDefault();
this.setState({answer_expose: value});
this.props.onExposeInstance(value, () => {
if(value === "tunnel"){
const waitForDomain = (count = 0) => {
return this.props.tunnelCall().then((url) => {
if(url && /\.filestash\.app$/.test(url) === true){
return Promise.resolve(url);
}
if(count > 10){
this.setState({working_message: "Building your domain"});
}else if(count > 30){
this.setState({working_message: "Processing ."+".".repeat(count % 3)});
}
if(count >= 60){
return Promise.reject({message: "Couldn't create a domain name"});
}
return new Promise((done) => window.setTimeout(done, 1000))
.then(() => waitForDomain(count + 1));
});
};
waitForDomain().then((url) => {
this.setState({redirect_uri: url});
}).catch((err) => {
window.location.hash = "#2";
window.location.reload();
});
} else {
this.setState({current: 2, has_answered_expose: true}, () => {
this.onStepChange(2);
});
}
this.setState({has_answered_password: true});
this.onStepChange(1);
});
}
onStepChange(n){
this.setState({current: n});
if(n === 2){
this.fetchDependencies();
}
}
fetchDependencies() {
this.props.summaryCall().then((deps) => {
this.setState({deps: deps});
this.setState({current: n}, () => {
if(n === 1) this.componentDidMount();
});
}
@ -242,11 +164,11 @@ class MultiStepForm extends React.Component {
return (
<div id="step1">
<FormStage navleft={false} navright={this.state.has_answered_password === true} current={this.state.current} onStepChange={this.onStepChange.bind(this)}>
Step 1/2: Secure your instance
Admin Password
</FormStage>
<ReactCSSTransitionGroup transitionName="stepper-form" transitionEnterTimeout={600} transitionAppearTimeout={600} transitionAppear={true} transitionEnter={true} transitionLeave={false}>
<div key={this.state.current}>
<p>Create your admin password: </p>
<p>Create your instance admin password: </p>
<form onSubmit={this.onAdminPassword.bind(this)}>
<Input ref="$input" type="password" placeholder="Password" value={this.state.answer_password} onChange={(e) => this.setState({answer_password: e.target.value})}/>
<Button theme="transparent">
@ -261,45 +183,6 @@ class MultiStepForm extends React.Component {
} else if(this.state.current === 1) {
return (
<div id="step2">
<FormStage navleft={true} navright={this.state.has_answered_expose} current={this.state.current} onStepChange={this.onStepChange.bind(this)}>
Step 2/2: Expose your instance to the internet ?
</FormStage>
<ReactCSSTransitionGroup transitionName="stepper-form" transitionEnterTimeout={600} transitionAppearTimeout={600} transitionAppear={true} transitionEnter={true} transitionLeave={false}>
<div key={this.state.current}>
<NgIf cond={this.state.redirect_uri !== null}>
<div style={{textAlign: "center"}}>
Your instance is available at <a href={this.state.redirect_uri}>{this.state.redirect_uri}</a>.<br/>
You will be redirected in <Countdown max={9} onZero={() => window.location.href = this.state.redirect_uri} /> seconds
</div>
</NgIf>
<NgIf cond={!this.props.loading && this.state.redirect_uri === null}>
<form onSubmit={this.onExposeInstance.bind(this, "skip")}>
<label className={this.state.answer_expose === "nothing" ? "active" : ""}>
<input type="radio" name="expose" value="nothing" checked={this.state.answer_expose === "nothing"} onChange={this.onExposeInstance.bind(this, "nothing")}/>
No, don't expose anything to the internet
</label>
<label className={this.state.answer_expose === "tunnel" ? "active" : ""}>
<input type="radio" name="expose" value="tunnel" checked={this.state.answer_expose === "tunnel"} onChange={this.onExposeInstance.bind(this, "tunnel")}/>
Yes, and make it available via a filestash subdomain - eg: https://user-me.filestash.app
</label>
<label className={this.state.answer_expose === "skip" ? "active" : ""}>
<input type="radio" name="expose" value="skip" checked={this.state.answer_expose === "skip"} onChange={this.onExposeInstance.bind(this, "skip")}/>
Skip if you're a wizard when it comes to SSL certificates and port forwarding
</label>
</form>
</NgIf>
<NgIf cond={!!this.props.loading && this.state.redirect_uri === null}>
<Loader/>
<div style={{textAlign: "center"}}>{this.state.working_message}</div>
</NgIf>
</div>
</ReactCSSTransitionGroup>
{hideMenu}
</div>
);
} else if(this.state.current === 2) {
return (
<div id="step3">
<FormStage navleft={true} navright={false} current={this.state.current} onStepChange={this.onStepChange.bind(this)}>
Summary
</FormStage>
@ -339,33 +222,6 @@ const FormStage = (props) => {
);
};
class Countdown extends React.Component {
constructor(props){
super(props);
this.state = { count: props.max };
}
componentDidMount(){
this.timeout = window.setInterval(() => {
if(this.state.count - 1 >= 0){
this.setState({count: this.state.count - 1}, () => {
if(this.state.count === 0) this.props.onZero();
});
}
}, 1000);
}
componentWillUnmount(){
window.clearInterval(this.timeout);
}
render(){
return(
<span>{this.state.count}</span>
);
}
}
function objectGet(obj, paths){
let value = obj;
for(let i=0; i<paths.length; i++){

View file

@ -29,18 +29,6 @@
@include inlinedInputWithSubmit();
}
#step2{
label{
display: block;
background: #f2f3f5;
margin: 10px 0;
padding: 10px 10px;
border-radius: 3px;
input[type="radio"]{ display: none; }
transition: background 0.05s ease;
&:hover, &.active{ background: var(--emphasis-primary); }
}
}
#step3{
.component_dependency_installed{
margin: 10px 0;
padding: 10px 10px;

View file

@ -1,7 +1,7 @@
package plugin
import (
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_starter_tunnel"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_starter_http"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_starter_tor"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_console"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_video_transcoder"

View file

@ -1,171 +0,0 @@
package plg_starter_tunnel
/*
* This plugin was inspired from the Inlets project: https://github.com/alexellis/inlets
* which is nothing but a wrapper on: github.com/rancher/remotedialer. As such we're using the parent
* project directly, avoiding unecessary dependencies. Interesting alternatives would include:
* - https://github.com/koding/tunnel using yamux which looks very interesting
* - https://github.com/mmatczuk/go-http-tunnel
* - https://github.com/jpillora/chisel
*/
import (
"context"
"github.com/gorilla/mux"
. "github.com/mickael-kerjean/filestash/server/common"
"github.com/mickael-kerjean/remotedialer"
"io/ioutil"
"net/http"
"time"
)
var backoff func() time.Duration = backoff_strategy()
func init() {
tunnel_enable := func() bool {
return Config.Get("features.server.tunnel_enable").Schema(func(f *FormElement) *FormElement{
if f == nil {
f = &FormElement{}
}
f.Default = false
f.Name = "tunnel_enable"
f.Type = "enable"
f.Target = []string{"tunnel_url"}
f.Description = "Enable/Disable tunnel for secure access from the internet"
f.Placeholder = "Default: false"
return f
}).Bool()
}
tunnel_enable()
Config.Get("features.server.tunnel_url").Schema(func(f *FormElement) *FormElement{
if f == nil {
f = &FormElement{}
}
f.Id = "tunnel_url"
f.Name = "tunnel_url"
f.Type = "text"
f.Target = []string{}
f.Description = "URL from which you can access your filestash. Contact us to make it more friendly"
f.ReadOnly = true
f.Placeholder = "LOADING... Refresh the page in a few seconds"
return f
})
Hooks.Register.Starter(func (r *mux.Router) {
Log.Info("[http] starting ...")
srv := &http.Server{
Addr: ":8334",
Handler: r,
}
go ensureAppHasBooted("http://127.0.0.1:8334/about", "[http] listening on :8334")
go func() {
if err := srv.ListenAndServe(); err != nil {
Log.Error("error: %v", err)
return
}
}()
Config.Get("features.server.tunnel_url").Set(nil)
if tunnel_enable() == false {
startTunnel := false
onChange := Config.ListenForChange()
for {
select {
case <- onChange.Listener: startTunnel = tunnel_enable()
}
if startTunnel == true { break }
}
Config.UnlistenForChange(onChange)
}
Log.Info("[tunnel] starting ...")
go func() {
for {
// Stage1: Register a domain name from which Filestash will be available
req, err := http.NewRequest("GET", "https://tunnel.filestash.app/register", nil);
if err != nil {
Log.Info("[tunnel] registration_request %s", err.Error())
time.Sleep(backoff())
continue
}
req.Header.Add("X-Machine-ID", GenerateMachineID())
res, err := HTTP.Do(req)
if err != nil {
Log.Info("[tunnel] registration_error %s", err.Error())
time.Sleep(backoff())
continue
} else if res.StatusCode != http.StatusOK {
Log.Info("[tunnel] registration_failure HTTP status: %d", res.StatusCode)
time.Sleep(backoff())
continue
}
d, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
// Stage2: Tunnel data from
remotedialer.ClientConnect(
"wss://tunnel.filestash.app/connect",
http.Header{
"X-Machine-ID": []string{ GenerateMachineID() },
},
nil,
func(proto, address string) bool {
if proto == "tcp" && address == "127.0.0.1:8334" {
return true
}
return false
},
func(context.Context) error {
Log.Info("[tunnel] started %s", string(d))
Config.Get("features.server.tunnel_url").Set(string(d))
return nil
},
)
Log.Info("[tunnel] closed")
time.Sleep(backoff())
Log.Info("[tunnel] restarting ...")
}
}()
})
}
func backoff_strategy() func() time.Duration {
var last_error time.Time = time.Now()
var last_wait time.Duration = 1 * time.Second
return func() time.Duration {
timeSinceLastError := time.Now().Sub(last_error)
last_error = time.Now()
if timeSinceLastError < 60 * time.Minute {
if last_wait < 60 * time.Second {
last_wait *= 2
}
return last_wait
}
last_wait = 1 * time.Second
return last_wait
}
}
func ensureAppHasBooted(address string, message string) {
i := 0
for {
if i > 10 {
Log.Warning("[http] didn't boot")
break
}
time.Sleep(250 * time.Millisecond)
res, err := http.Get(address)
if err != nil {
i += 1
continue
}
res.Body.Close()
if res.StatusCode != http.StatusOK {
i += 1
continue
}
Log.Info(message)
break
}
}