Ezt fejezzük be, de fejezzük be nagyszerűen.

bevezetés

Az elmúlt körülbelül három hónapban valóban mindent kidolgoztam egy webfejlesztési keretrendszeren, amelyet Julia számára hoztam létre, toollips néven, a hozzá tartozó ökoszisztémával együtt. Az ökoszisztéma jelenleg elég nagy, mondhatnám, hat elkészült csomagot tartalmaz, és még több készülőben van.

Bár ezek egy kicsit félre lettek tolva, mivel egyszerűen rengeteget kell fejlesztenem, nem látom, hogy olyan sok idő telik el, amíg mindkét első sor elkészül és indulásra kész lesz. Az egyik csomag, amely nagyon közel áll a kiadáshoz, a ToolipsRemote.



Csak néhány frissítést szerettem volna végrehajtani ezen a csomagon, mielőtt közzétenném,

  • Távoli kapcsolatok
  • Felhasználói csoportok

Ezzel a koncepcióval azt vettem észre, hogy sokat beszéltem a toollips szerverbővítmények készítéséről, sőt a blogomon is készítettem néhányat. Az eszközsorok kiterjesztésének azonban több módja van, mint a szerverbővítmények és a reprodukálható komponensek létrehozása! Az egyik ilyen mód a Connection. Ma megmutatom, hogyan bővíthetjük ki a AbstractConnection típust, hogy megváltoztassuk a toollips működését különböző körülmények között a csomag becsomagolásával!

felhasználói csoportok

Gyorsan ismerjük meg a felhasználói csoportok létrehozásának módját. Íme a távoli bővítmény jelenlegi mezői:

type::Vector{Symbol}
    remotefunction::Function
    f::Function
    logins::Dict{String, Vector{UInt8}}
    users::Dict{Vector{UInt8}, String}
    motd::String

Ezt egy kicsit módosítani kell, hogy különböző felhasználói csoportokat hozzunk létre, mert a különböző felhasználói csoportoknak eltérő távoli funkciókkal kell rendelkezniük. Ennek ellenére kissé módosítom az adataimat, hogy megkönnyítsem az ilyesmit. Vessünk egy pillantást a belső konstruktor bemeneteire:

function Remote(remotefunction::Function = controller(),
        users::Vector{Pair{String, String}} = ["root" => "1234"];
        motd::String = """### login to toolips remote session""",
        serving_f::Function = serve_remote)
  • A remotefunction az a függvény, amely mindegyiket meghívja, miután valaki bejelentkezett a megadott karakterlánccal a REPL-ből.
  • A users are olyan párok vektora, amelyek karakterláncként tartalmazzák a felhasználóneveket és jelszavakat. Ez csak a toollips remote indítására vonatkozik, a bejelentkezéseket vagy aktívan módosítani és tárolni kell, vagy környezeti változók biztosítják.
  • A motd a nap üzenete, amely akkor jelenik meg, amikor a felhasználó először csatlakozik.
  • Az serving_f az a funkció, amellyel a felhasználók bejelentkeznek. Valószínűleg nyugodtan kijelenthetjük, hogy a legtöbb esetben nem fogunk ezen változtatni.

Mindezeket figyelembe véve az itt végrehajtandó módosítás a users vektor, amelyet Vector{Pair{String, String}}-ről Vector{Pair{String, Pair}}-ra fogok változtatni. Az új pár tartalmazza a jelszót és a felhasználói csoportot is. Meg kell változtatnunk a remotefunction értéket is, ezt inkább egy különböző funkciók szótárára cserélem, kulcsként a felhasználói csoportokkal. Így mindkét funkcióval csak indexelhetünk a távoli kiszolgálás érdekében. Menjünk előre, és kezdjük el megváltoztatni ezeket a mezőket.

type::Vector{Symbol}
    remotefunction::Dict{Int64, Function}
    f::Function
    logins::Dict{String, Vector{UInt8}}
    users::Dict{Vector{UInt8}, Pair{String, Int64}}
    motd::String

Most megváltoztatom a függvényt, hogy megfelelővé tegyem ezeket a mezőket. Kezdjük az érvekkel:

remotefunction::Dict{Int64, Function} = Dict(1 => controller())
users::Vector{Pair{String, Pair}} = ["root" => "1234" => 1]

Ezután ezt bevisszük az egész függvénybe:

function Remote(remotefunction::Dict{Int64, Function} = Dict(1 => controller()),
        users::Vector{Pair{String, Pair}} = ["root" => "1234" => 1];
        motd::String = """### login to toolips remote session""",
        serving_f::Function = serve_remote)
        logins::Dict{String, Vector{UInt8}} = Dict(
        [n[1] => sha256(n[2]) for n in users])
        users = Dict{Vector{UInt8}, Pair{String, Int64}}()
        f(r::Vector{AbstractRoute}, e::Vector{ServerExtension}) = begin
            r["/remote/connect"] = serving_f
        end
        new([:routing, :connection], remotefunction, f, logins, users,
         motd)::Remote
    end

Végül frissítenünk kell a serve_remote-t, hogy illeszkedjen ezekhez az új változásokhoz. Lekérjük a felhasználónk felhasználói csoportját, majd a remotefunction mezőt ezzel a csoporttal indexeljük a funkció biztosítása érdekében.

function serve_remote(c::Connection)
    message = getpost(c)
    keybeg = findall(":SESSIONKEY:", message)
    if length(keybeg) == 1
            keystart = keybeg[1][2] + 11
            key = message[keystart:length(message)]
            # cut out the session key if provided.
            message = message[1:keybeg[1][1] - 1]
            print(message)
        if sha256(key) in keys(c[:Remote].users)
            group = c[:Remote].users[sha256(key)][2]
            c[:Remote].remotefunction[group](c, message, c[:Remote].users[sha256(key)])
        else
            write!(c, "Key invalid.")
        end

Azt is észreveheti, hogy a távoli függvényhívás három különböző argumentumot vesz fel. Mondanom sem kell, ez nem ideális. Ezen fogunk változtatni az új távoli kapcsolattal. Ennek ellenére valószínűleg ehhez a felhasználói adatainkat szeretnénk kérni, ezért íme egy pillantás:

if sha256(key) in keys(c[:Remote].users)
            userinfo = c[:Remote].users[sha256(key)]
            c[:Remote].remotefunction[userinfo[2]](newc)
        else
            write!(c, "Key invalid.")
        end

Észreveheti, hogy most ezt a függvényt a newc hívja meg, amely még nem létezik. Ez lesz a mi RemoteConnection , úgyhogy folytassuk és készítsük el.

távoli kapcsolatok

Mielőtt megírnánk ezt az új kapcsolatot, hivatkozzunk a Toolips.AbstractConnection dokumentációjára, hogy egy kicsit többet megtudjunk a kapcsolatok egészéről.

help?> Toolips.AbstractConnection

Három különböző mezőt kell megadnunk a kapcsolatunkon, a kiszolgáló útvonalain, a kiszolgáló kiterjesztésein és a http-n. Készítsünk egy ilyen szerkezetet:

mutable struct RemoteConnection <: Toolips.AbstractConnection
    routes::Vector{Toolips.AbstractRoute}
    http::Any
    extensions::Vector{Toolips.ServerExtension}
end

A HTTP mezőnknek ebben az esetben tetszőlegesnek kell lennie, különben el kell érnünk a HTTP-t. Minden egyes felvenni kívánt felhasználóra vonatkozóan is rendelkezünk adatokkal. Ez természetesen csak a csoport és a felhasználónév lesz:

mutable struct RemoteConnection <: Toolips.AbstractConnection
    routes::Vector{Toolips.AbstractRoute}
    http::Any
    extensions::Vector{Toolips.ServerExtension}
    group::Int64
    name::String
end

Most készítünk egy belső konstruktort, amely a korábban meglévő adatokból, a Connection és a felhasználói adatokból létrehozza ezt.

mutable struct RemoteConnection <: Toolips.AbstractConnection
    routes::Vector{Toolips.AbstractRoute}
    http::Any
    extensions::Vector{Toolips.ServerExtension}
    group::Int64
    name::String
    function RemoteConnection(c::Connection, userdata::Pair{String, Int64})
        new(c.routes, c.http, c.extensions, userdata[2], userdata[1])
    end
end

Végül ezt a konstruktort visszahívjuk a serve_remote függvényünkben.

if sha256(key) in keys(c[:Remote].users)
            userinfo = c[:Remote].users[sha256(key)]
            newc = RemoteConnection(c, userinfo)
            c[:Remote].remotefunction[userinfo[2]](newc)
        else

Most létre kell hoznunk néhány küldeményt. Ebben az a fantasztikus, hogy megváltoztathatjuk a különböző összetevők írásmódját. Mivel a bejövő szöveg leértékelésként jelenik meg, egyszerűen write! konvertálhatjuk a komponenseket leértékeléssé! Először egy összefoglalót készítünk az összes komponensről, így ha olyan Összetevőt adunk meg, amely nem jeleníthető meg távolról, akkor csak az Összetevő szövegét kapjuk:

function write!(c::RemoteConnection, s::Component{<:Any})
    write!(c, s[:text])
end

Ezután írjunk egy másik függvényt egy div-hez. Ez a funkció --- szintaxisú elválasztókat biztosít a jelölésen belül.

function write!(c::RemoteConnection, s::Component{:div})
    write!(c, "---")
    [write!(c, child) for child in s[:children]]
    write!(c, "---")
end

Folytatjuk ezt a címsorokkal és félkövérrel szedve:

write!(c::RemoteConnection, s::Component{:h1}) = write!(c, "# $(s[:text])\n")
write!(c::RemoteConnection, s::Component{:h2}) = write!(c, "## $(s[:text])\n")
write!(c::RemoteConnection, s::Component{:h3}) = write!(c, "### $(s[:text])\n")
write!(c::RemoteConnection, s::Component{:h4}) = write!(c, "#### $(s[:text])\n")
write!(c::RemoteConnection, s::Component{:b}) = write!(c, "**$(s[:text])**")

Ugyanezt biztosítsuk a hiperhivatkozásoknál is, ehhez a a használatával.

write!(c::RemoteConnection, s::Component{:a}) = write!(c, "[$(s[:text])]($(s[:href]))")

Végül megváltoztatom a controller által létrehozott alapértelmezett függvényünket. Ez a függvény egy függvényt ad vissza súgómenüvel és dolgokkal.

function controller(commands::Dict{String, Function} = Dict("?" => helpme,
                    "logit" => logit))
    f(c::Connection, m::String) = begin
        args = [string(arg) for arg in split(m, ";")]
        cmd = args[1]
        if length(args) != 1
            args = args[2:length(args)]
        else
            args = Vector{String}()
        end
        write!(c, commands[cmd](args, c))
    end
    f
end

Ennek a funkciónak most egy Connection-t és a bemeneti karakterláncot is fel kell vennie – aminek most tudomásom szerint a távoli kapcsolaton belül kell lennie.

mutable struct RemoteConnection <: Toolips.AbstractConnection
    routes::Vector{Toolips.AbstractRoute}
    http::Any
    extensions::Vector{Toolips.ServerExtension}
    group::Int64
    name::String
    message::String
    function RemoteConnection(c::Connection, userdata::Pair{String, Int64},
        message::AbstractString = "")
        new(c.routes, c.http, c.extensions, userdata[2], userdata[1],
        string(message))::RemoteConnection
    end
end
newc = RemoteConnection(c, userinfo, message)
c[:Remote].remotefunction[userinfo[2]](newc)

Most változtassuk meg ezt a funkciót.

function controller(commands::Dict{String, Function} = Dict("?" => helpme,
                    "logit" => logit))
    f(c::RemoteConnection) = begin
        m = c.message
        args = [string(arg) for arg in split(m, ";")]
        cmd = args[1]
        if length(args) != 1
            args = args[2:length(args)]
        else
            args = Vector{String}()
        end
        write!(c, commands[cmd](args, c))
    end
    f
end

Elég egyszerű, most próbáljuk ki mindezt, és reméljük, hogy nem lesz hiba.

következtetés

Hozzunk létre egy új webalkalmazást.

julia> using Toolips
julia> Toolips.new_app("RemoteTest")

Ezt valójában kétszer kellett megcsinálnom, mert az aktivált környezet a ToolipsRemote környezet volt, amelyen még mindig 0.1.3-as toollips volt. A kód egy dolog kivételével szinte kompatibilis, de nekem kicsit jobban tetszik a projekt felépítése azzal, ahogy az újabb verzió csinálja.

hopp

module RemoteTest
using Toolips
using ToolipsRemote
# welcome to your new toolips project!
"""
home(c::Connection) -> _
--------------------
The home function is served as a route inside of your server by default. To
    change this, view the start method below.
"""
function home(c::Connection)
    write!(c, p("helloworld", text = "hello world!"))
end
fourofour = route("404") do c
    write!(c, p("404message", text = "404, not found!"))
end
routes = [route("/", home), fourofour]
extensions = Vector{ServerExtension}([Logger(), Remote()])
"""
start(IP::String, PORT::Integer, ) -> ::ToolipsServer
--------------------
The start function starts the WebServer.
"""
function start(IP::String = "127.0.0.1", PORT::Integer = 8000)
     ws = WebServer(IP, PORT, routes = routes, extensions = extensions)
     ws.start(); ws
end
end # - module

Próbáljuk ki ezt!

include("dev.jl")
ERROR: LoadError: MethodError: no method matching ToolipsRemote.Remote(::Dict{Int64, ToolipsRemote.var"#f#10"{Dict{String, Function}}})
Closest candidates are:
  ToolipsRemote.Remote() at ~/.julia/packages/ToolipsRemote/ktpKK/src/ToolipsRemote.jl:56
  ToolipsRemote.Remote(::Dict{Int64, Function}) at ~/.julia/packages/ToolipsRemote/ktpKK/src/ToolipsRemote.jl:56
  ToolipsRemote.Remote(::Dict{Int64, Function}, ::Vector{Pair{String, Pair}}; motd, serving_f) at ~/.julia/packages/ToolipsRemote/ktpKK/src/ToolipsRemote.jl:56

Egyszerűen hozzá kell adnom a paramétereket ehhez a konstruktorhoz:

remotefunction::Dict{Int64, Function} = Dict{Int64, Function}(1 => controller()),
        users::Vector{Pair{String, Pair{String, Int64}}} = ["root" => "1234" => 1];

Most megint,

include("dev.jl")
RROR: LoadError: MethodError: no method matching sha256(::Pair{String, Int64})
Closest candidates are:
  sha256(::Union{Tuple{Vararg{UInt8, N}} where N, AbstractVector{UInt8}}) at /opt/julia-1.6.3/julia-1.6.3/share/julia/stdlib/v1.7/SHA/src/SHA.jl:87
  sha256(::IO) at /opt/julia-1.6.3/julia-1.6.3/share/julia/stdlib/v1.7/SHA/src/SHA.jl:116
  sha256(::IO, ::Any) at /opt/julia-1.6.3/julia-1.6.3/share/julia/stdlib/v1.7/SHA/src/SHA.jl:116

Ez az a típusú hiba, amelyre számítottam, mert a felhasználók megváltoztak.

logins::Dict{String, Vector{UInt8}} = Dict(
[n[1] => sha256(n[2][1]) for n in users])

Szerencsére a legtöbbször bejelentkezést használnak, és ez a hiba ismét a szerkezetünkön belül volt.

julia> include("dev.jl")
  Activating project at `~/dev/toolips/RemoteTest`
WebServer
hosted at: http://127.0.0.1:8000
status: active
    routes
    /
404
/remote/connect
extensions
    Logger
ToolipsRemote.Remote

Most próbáljunk meg csatlakozni hozzá!

julia> connect("http://127.0.0.1:8000")
┌ Error: error handling request
│   exception =
│    MethodError: no method matching write!(::Connection, ::String)
│    Stacktrace:

Nagyon nagy hoppá. Elfelejtettem importálni a write!-et a toollips-ből, ezért felülírtuk.

import Toolips: write!

Szerencsére, ha most hibát kapunk, az nincs a távoli konstruktorunkban. Valószínűnek tartom, hogy minden alkalommal dobni fog, amikor megpróbáljuk használni a serve_remote -t, de meglátjuk, hogy ez így van-e.

julia> using ToolipsRemote
julia> connect("http://127.0.0.1:8000")
  login to toolips remote session
  –––––––––––––––––––––––––––––––––
user: root
password for root: 
[2022:09:06:21:53]: 🌷 toolips> 123
[2022:09:06:21:53]: 🌷 toolips> �e�Y B/�A~Hg��O��J?��~������z�
[2022:09:06:21:53]: 🌷 toolips> �gB��\v��U�g�6#ȳ��E��x��F�
"Your password was not found."
julia>

Lehet, hogy elfelejtettem az alapértelmezett jelszót. Valószínűleg jó ötlet lenne eltávolítani a jelszó naplózását, amikor csak lehetséges.

user: root
password for root: 
┌ Error: error handling request
│   exception =
│    KeyError: key UInt8[0x15, 0x61, 0x9c, 0x0c, 0x15, 0x79, 0x7c, 0xb3, 0x9d, 0x0f  …  0xbf, 0xf9, 0x07, 0xeb, 0x9c, 0x16, 0x1f, 0x23, 0x48, 0x27] not found
│    Stacktrace:
     [1] getindex(h::Dict{Vector{UInt8}, Pair{String, Int64}}, key::Vector{UInt8})
│       @ Base ./dict.jl:481
│     [2] serve_remote(c::Connection)

Tudtam, hogy a serve_remote problémákat fog okozni nekünk.

if sha256(usrpwd[2]) == c[:Remote].logins[string(usrpwd[1])]
                    key = randstring(16)
                    c[:Remote].users[sha256(key)][1] = usrpwd[1]
                    write!(c, "$(usrpwd[1]):$key")
                else

Amit ezúttal csináltam, az indexelésünket véletlenül getindexre cseréltem, nem pedig beállított indexre, mert csak megkerestem a felhasználók minden példányát, és indexeltem, nem sok figyelmet fordítva a kontextusra. Ennek kijavítása feltárja,

julia> include("dev.jl")
  Activating project at `~/dev/toolips/RemoteTest`
WebServer
hosted at: http://127.0.0.1:8000
status: active
    routes
    /
404
/remote/connect
extensions
    Logger
ToolipsRemote.Remote
julia> using ToolipsRemote
julia> connect("http://127.0.0.1:8000")
  login to toolips remote session
  –––––––––––––––––––––––––––––––––
user: root
password for root: 
  connection successful!
  ------------------------
REPL mode remote initialized. Press [ to enter and backspace to exit.
"Prompt(\"🔗 root> \",...)"

Végül.

Az utolsó dolog, amit meg akartam tenni, az a tesztírás a RemoteConnection oldalunkra. Ehhez egyszerűen lecserélem az 1. szintű funkciónkat az otthoni funkciónkra:

extensions = Vector{ServerExtension}([Logger(), Remote(Dict(1 => home))])

Most pedig térjünk haza, és írjunk néhány dolgot. Az argumentumhoz tartozó ::Connection annotációt AbstractConnection-vé is fogjuk tenni.

function home(c::AbstractConnection)
    mydivider = div("mydivider")
    push!(mydivider, h("helloworld", 1, text = "hello world!"))
    push!(mydivider, a("toolips-link", text = "toolips :)",
    href = "https://toolips.app"))
    write!(c, mydivider)
end

Most kapcsolódjunk:

julia> connect("http://127.0.0.1:8000")
  login to toolips remote session
  –––––––––––––––––––––––––––––––––
user: root
password for root: 
  connection successful!
  ------------------------
REPL mode remote initialized. Press [ to enter and backspace to exit.
"Prompt(\"🔗 root> \",...)"
🔗 root> f
  –-# hello world! toolips :) (https://toolips.app)–-

Szóval ez jól működött, de ehhez hozzá kell adnunk \ns-et. Szükségünk van arra is, hogy a \ns szó szerinti legyen a szerver oldalán, mivel a kliens leértékelési elemzője valószínűleg nem szó szerint értelmezi őket.

function write!(c::RemoteConnection, s::Component{:div})
    write!(c, "---" * R"\n")
    [write!(c, child) for child in s[:children]]
    write!(c, "---" * R"\n")
end
write!(c::RemoteConnection, s::Component{:h1}) = write!(c, "# $(s[:text])" * R"\n")
write!(c::RemoteConnection, s::Component{:h2}) = write!(c, "## $(s[:text]))" * R"\n")
write!(c::RemoteConnection, s::Component{:h3}) = write!(c, "### $(s[:text])" * R"\n")
write!(c::RemoteConnection, s::Component{:h4}) = write!(c, "#### $(s[:text])" * R"\n")

Próbáljuk ki ezt.

julia> connect("http://127.0.0.1:8000")
  login to toolips remote session
  –––––––––––––––––––––––––––––––––
user: root
password for root: 
  connection successful!
  ------------------------
REPL mode remote initialized. Press [ to enter and backspace to exit.
"Prompt(\"🔗 root> \",...)"
🔗 root> f
  –-\n# hello world!\ntoolips :) (https://toolips.app)–-\n

Ó nem, ez nem működött. De ne aggódjunk, ezeket a \nket csak a csatlakoztatott repl függvényünkön belül cseréljük le!

function connected_repl(name::AbstractString, url::String, key::String)
    send_up(s::String) = begin
        r = post("$url/remote/connect", s * ":SESSIONKEY:$key")
        display(Markdown.parse(replace(r, R"\n" => "\n")))
    end
    initrepl(send_up,
                    prompt_text="🔗 $name> ",
                    prompt_color = :cyan,
                    start_key='[',
                    mode_name="remote")
end

Most…

🔗 root> a
  ────────────────────────────────────────────────────────────────────
hello world!
  ≡≡≡≡≡≡≡≡≡≡≡≡≡≡
toolips :) (https://toolips.app)–-

Igen, szóval a legtöbb dolog működik – nem biztos benne, hogy most mi a baj az a-val, vagy hogy a REPL így jeleníti-e meg ezt a fajta leértékelést. Ettől függetlenül elkalandozom, mivel itt nem az apró csípések a fő jellemzők! Mondanom sem kell, hogy ez az új frissítés nagyon félelmetes, és a toollips távirányítóval a befejezéshez közeledve örülök. Őrültség elképzelni, hogy ez a bővítmény hogyan teszi lehetővé a webhelyek ÉS a távoli példányok ugyanazon funkcióval történő kiszolgálását. Ezzel a példával továbbra is ellátogathatunk weboldalunkra…

… és ugyanazt a tartalmat kapja! Remélhetőleg mások is vonzónak találták ezt, és örülök, hogy végre létrehoztam azt, ami egyben az ÉN első kapcsolat-kiterjesztésem. Az eredmények nagyon jók! Ezt a csomagot a következő héten belül közzéteszik a Julia General Registry-ben. Köszönöm hogy elolvastad!