include_guard "pppoe"

template pppoe {
    alias("_arg0") dev;
    alias("_arg1") username;
    alias("_arg2") password;
    alias("_arg3") pre_up_template;

    # Choose which NCD interpreter will be used for the pppd event scripts.
    var("/usr/local/badvpn/bin/badvpn-ncd") ncd_interpreter_path;

    # Retry point here.
    var("false") retrying;
    backtrack_point() retry_point;
    If (retrying) {
        sleep("5000");
    };
    retrying->set("true");

    # Create a temporary directory.
    concat("/run/ncd-pppoe-", dev) run_dir;
    run({"/bin/rm", "-rf", run_dir}, {});
    run({"/bin/mkdir", run_dir}, {"/bin/rm", "-rf", run_dir});

    # Build paths for pppd scripts and other files.
    concat(run_dir, "/ncd-request.socket") socket_path;
    concat(run_dir, "/pppoe.pid") pppoe_pid_path;
    concat(run_dir, "/pap-secrets") pap_secrets_path;
    concat(run_dir, "/chap-secrets") chap_secrets_path;
    concat(run_dir, "/pppd2.tdb") pppdb_path;
    concat(run_dir, "/resolv.conf") resolv_conf_path;
    concat(run_dir, "/script-auth-up") path_auth_up;
    concat(run_dir, "/script-auth-down") path_auth_down;
    concat(run_dir, "/script-auth-fail") path_auth_fail;
    concat(run_dir, "/script-ip-pre-up") path_ip_pre_up;
    concat(run_dir, "/script-ip-up") path_ip_up;
    concat(run_dir, "/script-ip-down") path_ip_down;
    concat(run_dir, "/script-ipv6-up") path_ipv6_up;
    concat(run_dir, "/script-ipv6-down") path_ipv6_down;
    concat(run_dir, "/script-ipx-up") path_ipx_up;
    concat(run_dir, "/script-ipx-down") path_ipx_down;

    # Write secrets files.
    call("pppoe__write_secrets", {pap_secrets_path, username, password});
    call("pppoe__write_secrets", {chap_secrets_path, username, password});

    # Write pppd scripts. These will contact us via the request socket.
    call("pppoe__write_script", {"ip-pre-up", path_ip_pre_up});
    call("pppoe__write_script", {"ip-up", path_ip_up});
    call("pppoe__write_script", {"ip-down", path_ip_down});

    # Build path arguments for pppd;
    concat("pid-dir=", run_dir) arg_pid_dir;
    concat("pap-secrets=", pap_secrets_path) arg_pap_secrets;
    concat("chap-secrets=", chap_secrets_path) arg_chap_secrets;
    concat("pppdb=", pppdb_path) arg_pppdb;
    concat("resolv.conf=", resolv_conf_path) arg_resolv_conf;
    concat("auth-up=", path_auth_up) arg_auth_up;
    concat("auth-down=", path_auth_down) arg_auth_down;
    concat("auth-fail=", path_auth_fail) arg_auth_fail;
    concat("ip-pre-up=", path_ip_pre_up) arg_ip_pre_up;
    concat("ip-up=", path_ip_up) arg_ip_up;
    concat("ip-down=", path_ip_down) arg_ip_down;
    concat("ipv6-up=", path_ipv6_up) arg_ipv6_up;
    concat("ipv6-down=", path_ipv6_down) arg_ipv6_down;
    concat("ipx-up=", path_ipx_up) arg_ipx_up;
    concat("ipx-down=", path_ipx_down) arg_ipx_down;

    # Create state variables and blockers. When the request server
    # receives requests it will update those variables and blockers.
    var("down") state;
    var("") current_ifname;
    var("") current_local_ip;
    var("") current_remote_ip;
    value({}) current_dns_servers;
    blocker() ip_pre_up_blocker;
    blocker() ip_pre_up_done_blocker;
    blocker() ip_up_blocker;

    # Start request server.
    sys.request_server({"unix", socket_path}, "pppoe__request_handler", {});

    # Start pppd.
    sys.start_process({
        "/usr/sbin/pppd", "nodetach", "plugin", "rp-pppoe.so", dev, "noipdefault", "hide-password",
        "usepeerdns", "user", username,
        "path", arg_pid_dir, "path", arg_pap_secrets, "path", arg_chap_secrets, "path", arg_pppdb,
        "path", arg_resolv_conf,
        "path", arg_auth_up, "path", arg_auth_down, "path", arg_auth_fail, "path", arg_ip_pre_up,
        "path", arg_ip_up, "path", arg_ip_down, "path", arg_ipv6_up, "path", arg_ipv6_down,
        "path", arg_ipx_up, "path", arg_ipx_down
    }, "", ["deinit_kill_time":"2000"]) pppd;

    # Start a process which will cause retrying when pppd dies.
    spawn("pppoe__pppd_wait", {});

    # Wait for ip-pre-up.
    ip_pre_up_blocker->use();

    # Grab the current state variables, so the user doesn't
    # see any unexpected changes.
    var(current_ifname) ifname;
    var(current_local_ip) local_ip;
    var(current_remote_ip) remote_ip;
    var(current_dns_servers) dns_servers;

    # Call pre-up callback template.
    call_with_caller_target(pre_up_template, {ifname, local_ip, remote_ip, dns_servers}, "_caller");

    # Allow pre-up script to terminate.
    ip_pre_up_done_blocker->up();

    # Wait for connection to go up.
    ip_up_blocker->use();
}

template pppoe__pppd_wait {
    # Wait for pppd to die.
    _caller.pppd->wait();

    # Retry.
    _caller.retry_point->go();
}

template pppoe__write_secrets {
    alias("_arg0") file_path;
    alias("_arg1") username;
    alias("_arg2") password;

    # Escape username and password.
    regex_replace(username, {"\""}, {"\\\""}) username_esc;
    regex_replace(password, {"\""}, {"\\\""}) password_esc;

    # Write empty file and chmod it.
    file_write(file_path, "");
    run({"/bin/chmod", "600", file_path}, {});

    # Build contents.
    concat("\"", username_esc, "\" * \"", password_esc, "\"\n") contents;

    # Write file.
    file_write(file_path, contents);
}

template pppoe__write_script {
    alias("_arg0") event;
    alias("_arg1") script_path;

    # This string is an NCD script which will be run by pppd.
    # When run, it will contact us via the request server,
    # and the requests will be processed in pppoe__request_handler.
    var("#!<NCD_INTERPRETER_PATH>

process main {
    # Hardcoded strings.
    var(\"<EVENT>\") hardcoded_event;
    var(\"<SOCKET>\") hardcoded_socket;

    # Start timeout to kill us after some time if we don't manage
    # to contact the server.
    spawn(\"timeout_process\", {});

    # Build event data map.
    getargs() args;
    value([\"EVENT\":hardcoded_event, \"ARGS\":args]) msg_map;
    var({\"DEVICE\", \"IFNAME\", \"IPLOCAL\", \"IPREMOTE\", \"PEERNAME\", \"LOCALNAME\",
         \"SPEED\", \"ORIG_UID\", \"PPPLOGNAME\", \"CONNECT_TIME\", \"BYTES_SENT\",
         \"BYTES_RCVD\", \"LINKNAME\", \"DNS1\", \"DNS2\", \"WINS1\", \"WINS2\"}) var_names;
    Foreach (var_names As var_name) {
        getenv(var_name) env;
        If (env.exists) {
            msg_map->insert(var_name, env);
        };
    };

    # Connect to socket.
    sys.request_client({\"unix\", hardcoded_socket}) client;

    # Send request.
    client->request(msg_map, \"reply_handler\", \"finished_handler\", {});
}

template reply_handler {
    print();
}

template finished_handler {
    # Request was received by server, exit now.
    exit(\"0\");
}

template timeout_process {
    # Sleep some time.
    sleep(\"5000\");
    # Timed out, exit now.
    exit(\"1\");
}

"   ) script_contents_template;

    # Replace some constants in the script with the right values.
    regex_replace(script_contents_template,
                  {"<NCD_INTERPRETER_PATH>", "<EVENT>", "<SOCKET>"},
                  {_caller.ncd_interpreter_path, event, _caller.socket_path}
    ) script_contents;

    # Write the script.
    file_write(script_path, script_contents);

    # Make it executable.
    run({"/bin/chmod", "+x", script_path}, {});
}

template pppoe__request_handler {
    alias("_caller") pppoe;

    # Get event type from request.
    value(_request.data) request;
    request->get("EVENT") event;

    # Match to known types.
    val_equal(event, "ip-down") is_ip_down;
    val_equal(event, "ip-pre-up") is_ip_pre_up;
    val_equal(event, "ip-up") is_ip_up;

    If (is_ip_down) {
        # Set state.
        pppoe.state->set("down");

        # Set blockers down.
        pppoe.ip_up_blocker->down();
        pppoe.ip_pre_up_done_blocker->down();
        pppoe.ip_pre_up_blocker->down();
    }
    Elif (is_ip_pre_up) {
        # Expecting to be in "down" state here.
        val_different(pppoe.state, "down") state_is_wrong;
        If (state_is_wrong) {
            pppoe.retry_point->go();
            _request->finish();
        };

        # Get variables from request.
        request->get("IFNAME") ifname;
        request->get("IPLOCAL") local_ip;
        request->get("IPREMOTE") remote_ip;
        request->try_get("DNS1") dns1;
        request->try_get("DNS2") dns2;

        # Write variables.
        pppoe.current_ifname->set(ifname);
        pppoe.current_local_ip->set(local_ip);
        pppoe.current_remote_ip->set(remote_ip);
        pppoe.current_dns_servers->reset({});
        If (dns1.exists) {
            pppoe.current_dns_servers->insert(pppoe.current_dns_servers.length, dns1);
        };
        If (dns2.exists) {
            pppoe.current_dns_servers->insert(pppoe.current_dns_servers.length, dns2);
        };

        # Set state.
        pppoe.state->set("pre-up");

        # Set ip-pre-up blocker up.
        pppoe.ip_pre_up_blocker->up();

        # Wait for pre-up to be finished. This causes the script contacting
        # us to not return until then, and effectively delays pppd in setting
        # the device up and calling the ip-up script.
        pppoe.ip_pre_up_done_blocker->use();
    }
    Elif(is_ip_up) {
        # Expecting to be in "pre-up" state here.
        val_different(pppoe.state, "pre-up") state_is_wrong;
        If (state_is_wrong) {
            pppoe.retry_point->go();
            _request->finish();
        };

        # Set state.
        pppoe.state->set("up");

        # Set ip-up blocker up.
        pppoe.ip_up_blocker->up();
    };

    # Finish request.
    _request->finish();
}

template pppoe__escapeshellarg {
    alias("_arg0") input;
    regex_replace(input, {"'"}, {"\\'"}) replaced;
    concat("'", replaced, "'") result;
}
