├── .formatter.exs
├── .gitattributes
├── .github
└── workflows
│ └── elixir.yml
├── .gitignore
├── .travis.yml
├── CNAME
├── COC.md
├── CONTRIBUTORS.md
├── HISTORY.md
├── LICENSE
├── README.md
├── img
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
└── site.webmanifest
├── include
├── ftp.hrl
├── io.hrl
├── mqtt.hrl
├── n2o.hrl
├── n2o_api.hrl
├── n2o_core.hrl
├── n2o_info.hrl
├── n2o_pi.hrl
└── n2o_proc.hrl
├── index.html
├── lib
├── N2O.ex
├── aes_gcm.ex
└── ftp.ex
├── man
├── N2O.svg
├── WebSocket + MQTT.svg
├── bert.js.htm
├── ftp.js.htm
├── heart.js.htm
├── ieee754.js.htm
├── mq.js.htm
├── n2o.htm
├── n2o.js.htm
├── n2o_auth.htm
├── n2o_bert.htm
├── n2o_cowboy.htm
├── n2o_ftp.htm
├── n2o_gproc.htm
├── n2o_heart.htm
├── n2o_json.htm
├── n2o_mqtt.htm
├── n2o_pi.htm
├── n2o_proto.htm
├── n2o_ring.htm
├── n2o_secret.htm
├── n2o_session.htm
├── n2o_static.htm
├── n2o_syn.htm
├── n2o_ws.htm
├── utf8.js.htm
└── zlib.js.htm
├── mix.exs
├── priv
├── bert.js
├── ftp.js
├── heart.js
├── ieee754.js
├── mq.js
├── n2o.js
├── utf8.js
├── xhr.js
└── zlib.js
├── rebar.config
├── src
├── mqtt
│ └── n2o_mqtt.erl
├── n2o.app.src
├── n2o.erl
├── n2o_multipart.erl
├── n2o_pi.erl
├── n2o_proto.erl
├── n2o_ring.erl
├── protos
│ ├── n2o_ftp.erl
│ └── n2o_heart.erl
├── services
│ ├── n2o_bert.erl
│ ├── n2o_json.erl
│ ├── n2o_secret.erl
│ ├── n2o_session.erl
│ └── n2o_xml.erl
└── ws
│ ├── n2o_cowboy.erl
│ ├── n2o_gproc.erl
│ ├── n2o_static.erl
│ ├── n2o_syn.erl
│ └── n2o_ws.erl
├── sys.config
└── test
├── bert.sh
├── bert_gen.erl
├── bert_test.js
├── casper
└── casper.js
├── elements.erl
├── index.html
├── n2o_SUITE.erl
└── test.hrl
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
4 | line_length: 80,
5 | export: [
6 | locals_without_parens: [some_dsl_call: 2, some_dsl_call: 3]
7 | ]
8 | ]
9 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.html linguist-detectable=false
2 | *.htm linguist-detectable=false
3 | *.js linguist-detectable=false
4 | *.css linguist-detectable=false
5 | *.sh linguist-detectable=false
6 |
--------------------------------------------------------------------------------
/.github/workflows/elixir.yml:
--------------------------------------------------------------------------------
1 | name: mix
2 | on: push
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v2
8 | - uses: erlef/setup-elixir@v1
9 | with:
10 | otp-version: 24.x
11 | elixir-version: 1.13.x
12 | - name: Dependencies
13 | run: |
14 | mix local.rebar --force
15 | mix local.hex --force
16 | mix deps.get
17 | - name: Compilation
18 | run: mix compile
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ebin
2 | .DS_Store
3 | .applist
4 | Mnesia.nonode@nohost
5 | _build
6 | log
7 | deps
8 | *.toc
9 | *.log
10 | *.aux
11 | *.pdf
12 | *.lock
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: erlang
2 | otp_release:
3 | - 20.3.8
4 | - 21.3
5 | - 22.0
6 | - 23.0
7 | notifications:
8 | email:
9 | - maxim@synrc.com
10 | script:
11 | - "rebar get-deps clean compile"
12 | - "curl -fsSL https://raw.github.com/synrc/mad/master/mad > mad"
13 | - "chmod +x mad"
14 | - "./mad dep com"
15 | - "rebar3 dialyzer"
16 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | ws.n2o.dev
2 |
--------------------------------------------------------------------------------
/COC.md:
--------------------------------------------------------------------------------
1 | # N2O User Agreement
2 |
3 | ## 1. Motivation
4 |
5 | Maximal inclusion.
6 | We do care your sex, race, nationality, split config,
7 | be whoever you want to be and manifest
8 | yourself in as many mindstreams as you wish.
9 |
10 | ## 2. Openness
11 |
12 | We are open.
13 |
14 | ## 3. Expected Behavior
15 |
16 | * Healthy contribution;
17 | * Respect us and we will respect you;
18 | * Positive first, negative next (if needed);
19 | * Mindfulness;
20 | * General socially accepted behavior.
21 |
22 | ## 4. Unacceptable Behavior
23 |
24 | * Violence;
25 | * Discriminatory jokes and language;
26 | * Discrimination by programming language;
27 | * Jokes about body;
28 | * Familiarity nicknames;
29 | * Doxing;
30 | * Sexual misconduct;
31 | * Annoing behavior;
32 | * Criminal behavior.
33 |
34 | ## 6. Consequences of Unacceptable Behavior
35 |
36 | Ban.
37 |
38 | ## 7. Reporting Guidelines
39 |
40 | Talk to @5HT.
41 |
42 | ## 8. Addressing Grievances
43 |
44 | N2O board committee is higher that @5HT.
45 |
46 | ## 9. Scope
47 |
48 | Offline and Online.
49 |
50 | ## 10. License and attribution
51 |
52 | This contract is distributed under the terms of ISC license.
53 |
54 | ## 11. Contact info
55 |
56 | Namdak Tonpa @5HT
57 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # Credits
2 |
3 | * Maxim Sokhatsky — core, shen, windows
4 | * Dmitry Bushmelev — yaws, cowboy
5 | * Andrii Zadorozhnii — elements, actions
6 | * Vladimir Kirillov — mac, bsd, xen, linux
7 | * Andrey Martemyanov — bert
8 | * Oleksandr Nikitin — security
9 | * Anton Logvinenko — doc
10 | * Roman Shestakov — elements, ct
11 | * Jesse Gumm — nitrogen, help
12 | * Yuri Maslovsky — mqtt
13 | * Igor Kharin — k8s, ekka
14 | * Rusty Klophaus — original author
15 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | # History
2 |
3 | * 4.5 — Basic Messaging with single topic and Echo
4 | * 4.6 — Echo eliminated Basic Filter with req/rep topics
5 | * 4.7 — DHT supervised node for server MQTT connections
6 | * 4.9 — Subscribe offline clients
7 | * 4.9.3 — Cleaned n2o_async, written manpage
8 | * 5.10 — Cleanup
9 | * 5.11 — Formatter API changes
10 | * 5.11.1 — Unification of WebSocket and MQTT versions
11 | * 6.3 — arraybuffer in JS client, n2o_pi, fixes, synrc/mqtt drop
12 | * 6.4 — Error handling, minimalisation
13 | * 6.5 — OTP 21.0, n2o.dev, WebSocket ring, /-endpoint for static/WS, Erlang logger
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | DHARMA License
2 |
3 | Copyright (c) 2011—2022 Synrc Research
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | YOU CANNOT USE THIS SOFTWARE IN ANY (PROVABLE BY MONEY TRACE)
10 | PROCESS CHAIN OF EXTERMINATING UKRAINIANS BY ANY MEANS OF FASCIST
11 | ACTIONS AGAINST OUR TERRITORIAL INTEGRITY, CULTURAL DIVERSITY BY
12 | APPLYING MILITARY INVASIONS, ECONOMICAL WARS, HUMANITARIAN DISASTERS,
13 | ARTFICIAL HOLODOMORS, GENOCIDE, RAPING, LOOTING, ROBBERIES, SPREADING
14 | FAKE INFORMATION, AND OTHER CONTEMPORARY WEAPONS OF WAR AT SCALE
15 | OR IN INVIDIVUAL MANNER.
16 |
17 | YOU CANNOT USE THIS SOFTWARE BY ANY MEANS IN INTEREST OF LEGAL
18 | ENTITIES OR INDIVIDUALS WHO IS SUPPORTING NOW OR WAS SUPPORTING
19 | BACK THEN FASCISM, RUSCISM, COMMUNISM, CHAUVINISM, HUMILIATION,
20 | AND OTHER SUPPRESSIVE IDEOLOGIES IN DIFFERENT EXPRESSIONS.
21 |
22 | STOP KILLING UKRAINIANS,
23 | THE COUNTER RENDERS TENS OF MILLIONS.
24 |
25 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
26 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
27 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
28 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
29 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
30 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
31 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # N2O: TCP MQTT WebSocket
2 |
3 | [](https://github.com/synrc/n2o/actions)
4 | [](https://hex.pm/packages/n2o)
5 |
6 | N2O is an embeddable message protocol loop library for WebSocket, HTTP, MQTT
7 | and TCP servers. It provides basic features, such as process management,
8 | virtual nodes ring for request processing, sessions, frame encoding, and
9 | unified API for external mq and caching services.
10 |
11 | ## Core Features
12 |
13 | * Purpose: High performance protocol relay
14 | * Endpoints: WebSockets, MQTT, TCP
15 | * Codebase: 700 LOC (Erlang), 500 LOC (JavaScript)
16 | * Dialyzer: REBAR, REBAR3, MAD, MIX
17 | * Hosts: BANDIT, COWBOY, EMQ, MOCHIWEB, RING, TCP, UDP
18 | * PubSub: GPROC, SYN, PG2
19 | * Formatters: JSON, BERT, ASN.1
20 |
21 | ## Protocol Extensions
22 |
23 | * Templates: DTL, [NITRO](https://nitro.n2o.dev)
24 | * Abstract Database Layer [KVS](https://kvs.n2o.dev"): FS, MNESIA, ROCKSDB, RIAK, REDIS
25 | * Business Processes: [BPE](https://bpe.n2o.dev) (BPMN 2.0), SCM, ERP, CRM
26 | * HTTP API: [REST](https://rest.n2o.dev) (proplist/JSON)
27 | * [ACTIVE](https://active.n2o.dev) Reloading: GNU/Linux, Windows, macOS
28 |
29 | ## Basic Samples
30 |
31 | * MQTT Chat: [REVIEW TT](https://review.n2o.dev) (8000)
32 | * WebSocket Chat: [SAMPLE WS](https://sample.n2o.dev) (8001)
33 |
34 | ## Enterprise Samples
35 |
36 | * Online Client Bank: [BANK](https://fin.erp.uno) (8041)
37 | * Instant Messaging: [CHAT](https://chat.n2o.dev) (8042)
38 | * Product Lifecycle Management: [PLM](https://plm.erp.uno) (8043)
39 |
40 | ## Motivation
41 |
42 | N2O was created to bring clarity and sanity to software development.
43 | The distribution model is per file basis with ISC license.
44 |
45 | ## Kernel
46 |
47 | The core modules provide OTP start and N2O entry point.
48 |
49 | * [n2o](https://ws.n2o.dev/man/n2o.htm) — N2O OTP Supervisor and Application
50 | * [n2o_pi](https://ws.n2o.dev/man/n2o_pi.htm) — N2O Processes
51 | * [n2o_proto](https://ws.n2o.dev/man/n2o_proto.htm) — N2O Loop
52 | * [n2o_ring](https://ws.n2o.dev/man/n2o_ring.htm) — N2O Ring
53 |
54 | ## WebSocket
55 |
56 | N2O Loop is directly connected and ran inside context of WebSocket handler.
57 | Usually in Erlang we use `syn` or `gproc` OTP message buses.
58 | As such buses are optional in MQTT setup we include bus drivers in WebSocket package.
59 |
60 | * [n2o_ws](https://ws.n2o.dev/man/n2o_ws.htm) — N2O WebSocket Virtual Node
61 | * [n2o_heart](https://ws.n2o.dev/man/n2o_heart.htm) — PING protocol
62 | * [n2o_cowboy](https://ws.n2o.dev/man/n2o_cowboy.htm) — COWBOY API
63 | * [n2o_gproc](https://ws.n2o.dev/man/n2o_gproc.htm) — GPROC bus backend
64 | * [n2o_syn](https://ws.n2o.dev/man/n2o_syn.htm) — SYN bus backend
65 |
66 | ```sh
67 | git clone git@github.com:synrc/sample && cd sample
68 | rebar3 shell
69 | open open http://localhost:8001/app/login.htm
70 | ```
71 |
72 | ## Protocols
73 |
74 | N2O ships with 3 optional protocols.
75 |
76 | * [n2o_ftp](https://ws.n2o.dev/man/n2o_ftp.htm) — N2O File protocol
77 | * [n2o_heart](https://ws.n2o.dev/man/n2o_heart.htm) — N2O Heart protocol
78 | * [nitro_n2o](https://nitro.n2o.dev/man/nitro_n2o.htm) — Nitrogen Web Framework protocol
79 | * [bpe_n2o](https://bpe.n2o.dev) — Business Process Engine protocol
80 |
81 | ## Services
82 |
83 | Formatters, Sessions, etc. Optional.
84 |
85 | * [n2o_bert](https://ws.n2o.dev/man/n2o_bert.htm) — BERT encoder/decoder
86 | * [n2o_json](https://ws.n2o.dev/man/n2o_json.htm) — JSON encoder/decoder
87 | * [n2o_secret](https://ws.n2o.dev/man/n2o_secret.htm) — AES/GCM-256 encoder/decoder
88 | * [n2o_session](https://ws.n2o.dev/man/n2o_session.htm) — ETS session storage
89 |
90 | ## JavaScript
91 |
92 | * [bert.js](https://ws.n2o.dev/man/bert.js.htm) — BERT encoder/decoder
93 | * [utf8.js](https://ws.n2o.dev/man/utf8.js.htm) — UTF8 encoder/decoder
94 | * [ieee754.js](https://ws.n2o.dev/man/ieee754.js.htm) — IEEE754 encoder/decoder
95 | * [heart.js](https://ws.n2o.dev/man/heart.js.htm) — HEART protocol
96 | * [ftp.js](https://ws.n2o.dev/man/ftp.js.htm) — FTP protocol
97 | * [n2o.js](https://ws.n2o.dev/man/n2o.js.htm) — N2O protocol loop
98 | * [mq.js](https://ws.n2o.dev/man/mq.js.htm) — MQTT client
99 |
100 | ## Literature
101 |
102 | * "N2O: no bullshit sane framework for wild web" [PDF](https://n2o.dev/books/n2o.pdf) (versions 0.11—4.4)
103 | * "N2O BOOK Vol.2 Green Book" [HTML](https://n2o.dev/ua/books/vol.2/index.html) (versions 4.5—9.11)
104 |
--------------------------------------------------------------------------------
/img/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/synrc/n2o/d07cd152df573302bfaac8019af3903b5ae860fa/img/android-chrome-192x192.png
--------------------------------------------------------------------------------
/img/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/synrc/n2o/d07cd152df573302bfaac8019af3903b5ae860fa/img/android-chrome-512x512.png
--------------------------------------------------------------------------------
/img/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/synrc/n2o/d07cd152df573302bfaac8019af3903b5ae860fa/img/apple-touch-icon.png
--------------------------------------------------------------------------------
/img/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/synrc/n2o/d07cd152df573302bfaac8019af3903b5ae860fa/img/favicon-16x16.png
--------------------------------------------------------------------------------
/img/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/synrc/n2o/d07cd152df573302bfaac8019af3903b5ae860fa/img/favicon-32x32.png
--------------------------------------------------------------------------------
/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/synrc/n2o/d07cd152df573302bfaac8019af3903b5ae860fa/img/favicon.ico
--------------------------------------------------------------------------------
/img/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"img/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"img/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/include/ftp.hrl:
--------------------------------------------------------------------------------
1 | -ifndef(FTP_HRL).
2 | -define(FTP_HRL, true).
3 |
4 | -record(ftp, { id=[], sid=[], filename=[], meta=[], size=[], offset=[], block=[], data=[], status=[], options = []}).
5 | -record(ftpack, { id=[], sid=[], filename=[], meta=[], size=[], offset=[], block=[], data=[], status=[], options = []}).
6 |
7 | -endif.
8 |
--------------------------------------------------------------------------------
/include/io.hrl:
--------------------------------------------------------------------------------
1 | -ifndef(IO_HRL).
2 | -define(IO_HRL, true).
3 |
4 | -record(bin, { data=[] }).
5 | -record(client, { data=[] }).
6 | -record(server, { data=[] }).
7 |
8 | -endif.
9 |
--------------------------------------------------------------------------------
/include/mqtt.hrl:
--------------------------------------------------------------------------------
1 | -ifndef(MQTT_HRL).
2 | -define(MQTT_HRL, true).
3 |
4 | % MQTT listener state
5 | -record(mqcn, { conn :: gen_statem:start_ret()
6 | , proto = [] :: [] | list()
7 | }).
8 |
9 | % MQTT topics
10 | % * /:events/:erdpou/:service/:submodule/:node/:vsn/[:client_id] - n2o service listeners
11 | % :erdpou - service owner
12 | % :submodule - submodule, subprotocol or page
13 | % :client_id - optional client id. shrinks reply topic to '/actions/:submodule/:client_id'
14 | %
15 | % * /:actions/:page/:client_id - client listeners: js, etc.
16 |
17 | -define(B(L), n2o:to_binary(L)).
18 | -define(E(P,T), application:get_env(n2o,P,T)).
19 | -define(VSN, "1").
20 |
21 | -define(ACT_TOPIC(P), ?B([?E(action_topic,"/actions"),"/",P])).
22 | -define(ACT_TOPIC(P,Cid), ?B([?E(action_topic,"/actions"),"/",P,"/",Cid])).
23 |
24 | -define(EV_TOPIC(O,S,N), ?EV_TOPIC(O,S,"+",N)).
25 | -define(EV_TOPIC(O,S,M,N), ?B([?E(events_topic,"/events"),"/",?B(O),"/",S,"/",M,"/",N,"/",?VSN,"/#"])).
26 |
27 | -endif.
28 |
--------------------------------------------------------------------------------
/include/n2o.hrl:
--------------------------------------------------------------------------------
1 | -ifndef(N2O_HRL).
2 | -define(N2O_HRL, true).
3 |
4 | -define(FORMAT(F), case F of F when is_binary(F) -> binary_to_list(F);
5 | F when is_atom(F) -> atom_to_list(F);
6 | F when is_list(F) -> F end).
7 |
8 | -ifdef(OTP_RELEASE).
9 | -include_lib("kernel/include/logger.hrl").
10 | -else.
11 | -define(LOG_INFO(F), io:format(?FORMAT(F)) end).
12 | -define(LOG_INFO(F,X), io:format(?FORMAT(F),X)).
13 | -define(LOG_ERROR(F), io:format("{~p,~p}: ~p~n", [?MODULE,?LINE,F])).
14 | -define(LOG_ERROR(F,X), io:format(?FORMAT(F),X)).
15 | -endif.
16 |
17 | -define(LOG_EXCEPTION(E,R,S), ?LOG_ERROR(#{exception => E, reason => R, stack => S})).
18 |
19 | -record(pi, { name :: term(),
20 | table :: atom(),
21 | sup :: atom(),
22 | module :: atom(),
23 | timeout = 5000 :: integer(),
24 | restart = transient :: atom(),
25 | state :: term() }).
26 |
27 | -record(cx, { handlers = [] :: list({atom(),atom()}),
28 | actions = [] :: list(tuple()),
29 | req = [] :: [] | term(),
30 | module = [] :: [] | atom() | list(),
31 | lang = [] :: [] | atom(),
32 | path = [] :: [] | binary(),
33 | session = [] :: [] | binary(),
34 | token = [] :: [] | binary(),
35 | formatter = bert :: bert | json | atom(),
36 | params = [] :: [] | list(tuple()) | binary() | list(),
37 | node = [] :: [] | atom() | list(),
38 | client_pid= [] :: [] | term(),
39 | state = [] :: [] | term(),
40 | from = [] :: [] | binary(),
41 | vsn = [] :: [] | binary() }).
42 |
43 | -define(CTX(ClientId), n2o:cache(ClientId)).
44 | -define(REQ(ClientId), (n2o:cache(ClientId))#cx.req).
45 |
46 | % API
47 |
48 | -define(QUERING_API, [init/2, finish/2]).
49 | -define(SESSION_API, [init/2, finish/2, get_value/2, set_value/2, clear/0]).
50 | -define(MESSAGE_API, [send/2, reg/1, reg/2, unreg/1, init/0]).
51 |
52 | -define(N2O_JSON, (application:get_env(n2o,json,jsone))).
53 |
54 | % IO protocol
55 |
56 | -include_lib("n2o/include/io.hrl").
57 |
58 | % File Transfer Protocol
59 |
60 | -include_lib("n2o/include/ftp.hrl").
61 |
62 | % MQTT Services
63 |
64 | -include_lib("n2o/include/mqtt.hrl").
65 |
66 | -endif.
67 |
--------------------------------------------------------------------------------
/include/n2o_api.hrl:
--------------------------------------------------------------------------------
1 | -ifndef(N2O_API).
2 | -define(N2O_API, true).
3 |
4 | -include("n2o_core.hrl").
5 |
6 | -type memtable() :: atom().
7 |
8 | -spec encode(tuple()) -> binary().
9 | -spec decode(binary()) -> tuple().
10 | -spec session(term(),term()) -> term().
11 | -spec session(term()) -> term().
12 | -spec cache(memtable(),term(),term()) -> term().
13 | -spec cache(memtable(),term()) -> term().
14 |
15 | -endif.
16 |
--------------------------------------------------------------------------------
/include/n2o_core.hrl:
--------------------------------------------------------------------------------
1 | -ifndef(N2O_CORE).
2 | -define(N2O_CORE, true).
3 |
4 | -record(bert, { data :: term() }).
5 | -record(json, { data :: term() }).
6 | -record(binary, { data :: term() }).
7 | -record(default, { data :: term() }).
8 | -record(ok, { data :: term() }).
9 | -record(error, { data :: term() }).
10 | -record(reply, { msg :: n2o(), req :: term(), ctx :: cx() } ).
11 | -record(unknown, { msg :: n2o(), req :: term(), ctx :: cx() } ).
12 |
13 | -type n2o() :: #bert{} | #json{} | #binary{} | #default{}.
14 | -type cx() :: #cx{}.
15 | -type formatter() :: binary | json | bert | text | default | atom().
16 | -type response() :: { formatter(), binary() }.
17 |
18 | -endif.
19 |
--------------------------------------------------------------------------------
/include/n2o_info.hrl:
--------------------------------------------------------------------------------
1 | -ifndef(N2O_INFO).
2 | -define(N2O_INFO, true).
3 |
4 | -include("n2o_core.hrl").
5 |
6 | -spec info(term(),term(),#cx{}) -> #reply{} | #unknown{}.
7 |
8 | -endif.
9 |
--------------------------------------------------------------------------------
/include/n2o_pi.hrl:
--------------------------------------------------------------------------------
1 | -ifndef(N2O_PI).
2 | -define(N2O_PI, true).
3 |
4 | -include("n2o_core.hrl").
5 |
6 | -spec start(#pi{}) -> {pid(),term()} | #error{}.
7 | -spec stop(term(),atom()) -> #pi{} | #error{}.
8 |
9 | -endif.
10 |
--------------------------------------------------------------------------------
/include/n2o_proc.hrl:
--------------------------------------------------------------------------------
1 | -ifndef(N2O_PROC).
2 | -define(N2O_PROC, true).
3 |
4 | -include("n2o.hrl").
5 |
6 | -spec proc(atom(),#pi{}) -> term().
7 |
8 | -endif.
9 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | N2O
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 |
29 |
30 |
31 |
32 | N2O
33 |
34 |
35 |
36 |
37 | SYNOPSIS
38 | N2O is an embeddable message protocol loop library for
39 | WebSocket, MQTT and TCP servers. It provides basic
40 | features, such as: process management; virtual nodes ring for
41 | request processing; session, encoding, mq and cache services.
42 |
43 |
44 | NOV 2021 ©
5HT ISC
45 | VER 8.11 8.8 7.1
46 |
47 |
48 |
49 |
50 | USAGE
51 | Get the rebar dependency and compile it:
52 |
53 |
54 | $ mad get n2o
55 | $ cd deps/n2o
56 | $ mad dep com pla rep
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | Endpoints
66 |
67 | Here is a list of types of endpoints which are supported by EMQ and accessible to N2O apps:
68 | WebSockets, MQTT, MQTT-SN, TCP, UDP, CoAP.
69 | Normal use of N2O as a Web Framework or a Web Application Server is through WebSockets,
70 | but for IoT and MQTT applications it could be served through UDP or SCTP protocols,
71 | providing application level message delivery consistency.
72 | By using MQTT as a transport we extend the supported set of endpoint protocols.
73 |
74 |
75 |
76 | MQTT
77 | EMQ is an open-source MQTT broker implemented by Feng Lee.
78 | N2O is EMQ compatible plugin that acts as predefined MQTT clients that form
79 | a ring of virtual nodes, process all the protocol requests,
80 | and answer to client topics according to classical RPC over MQ schema.
81 | This list of modules forms a core of N2O that is common for
82 | both MQTT and WebSocket versions:
83 |
84 |
85 |
86 | n2o — N2O Application: MQTT and WS — 10KB
87 | n2o_pi — N2O Processes — 4KB
88 | n2o_proto — N2O Loop: — 1KB
89 | n2o_ring — N2O Ring: Hash Ring — 1KB
90 |
91 |
92 |
93 |
94 | $ mad app zero review
95 | $ cd review
96 | $ mad dep com pla rep
97 | $ open http://127.0.0.1:8000
98 |
99 |
100 |
101 |
The list of MQTT specific modules:
102 |
103 | n2o_auth — N2O MQTT Authentication: MQTT auth module — 1KB
104 | n2o_mqtt — MQTT DHT Processing Node
105 |
106 |
107 |
108 |
109 | WebSocket
110 | COWBOY is small, fast, and modern HTTP server implemented by Loïc Hoguin.
111 | In this version, N2O loop is integrated as WebSocket cowboy
112 | handler that routes all incoming messages to N2O.
113 | The list of WebSocket specific modules:
114 |
115 | n2o_cowboy — COWBOY API
116 | n2o_ws — WebSocket DHT Processing Node
117 |
118 |
119 |
120 | $ mad app nitro sample
121 | $ cd sample
122 | $ mad dep com pla rep
123 | $ open https://127.0.0.1:8001/app/index.htm
124 |
125 |
126 |
127 |
128 | Protocols
129 |
130 | n2o_heart — PING protocol
131 | n2o_ftp — N2O File Protocol: FTP — 4KB
132 | n2o_nitro — N2O Nitrogen Web Framework Protocol: — 3KB
133 |
134 |
135 |
136 | Services
137 |
143 |
144 |
145 | JavaScript
146 |
157 |
158 |
159 | SPEC
160 |
161 | N2O:
162 |
163 | -type n2o() :: #bert{} | #json{} | #binary{} | #default{}.
164 | -type cx() :: #cx{}.
165 | -type formatter() :: binary | json | bert | text | default | atom().
166 | -type response() :: { formatter(), binary() }.
167 |
168 | -record(ok, { data :: term() }).
169 | -record(error { data :: term() }.
170 | -record(reply, { msg :: n2o(), req :: term(), ctx :: cx() } ).
171 | -record(unknown, { msg :: n2o(), req :: term(), ctx :: cx() } ).
172 |
173 | -spec start(#pi{}) -> {pid(),term()} | #error{}.
174 | -spec stop(term(),atom()) -> #pi{} | #error{}.
175 | -spec proc(atom(),#pi{}) -> term().
176 | -spec info(term(),term(),#cx{}) -> #reply{} | #unknown{}.
177 |
178 | -record(pi, { name :: term(),
179 | table :: atom(),
180 | sup :: atom(),
181 | module :: atom(),
182 | state :: term() }).
183 |
184 | -record(cx, { handlers = [] :: list({atom(),atom()}),
185 | actions = [] :: list(tuple()),
186 | req = [] :: [] | term(),
187 | module = [] :: [] | atom(),
188 | lang = [] :: [] | atom(),
189 | path = [] :: [] | binary(),
190 | session = [] :: [] | binary(),
191 | formatter = bert :: bert | json | atom(),
192 | params = [] :: [] | list(tuple()),
193 | node = [] :: [] | atom(),
194 | client_pid = [] :: [] | term(),
195 | state = [] :: [] | term(),
196 | from = [] :: [] | binary(),
197 | vsn = [] :: [] | binary() }).
198 |
199 | -type memtable() :: atom().
200 | -spec encode(tuple()) -> binary().
201 | -spec decode(binary()) -> tuple().
202 | -spec session(term(),term()) -> term().
203 | -spec session(term()) -> term().
204 | -spec cache(memtable(),term(),term()) -> term().
205 | -spec cache(memtable(),term()) -> term().
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
--------------------------------------------------------------------------------
/lib/N2O.ex:
--------------------------------------------------------------------------------
1 | defmodule N2O do
2 | require Record
3 |
4 | Enum.each(Record.extract_all(from_lib: "n2o/include/n2o.hrl"), fn {name,
5 | definition} ->
6 | Record.defrecord(name, definition)
7 | end)
8 |
9 | defmacro __using__(opts \\ []) do
10 | imports =
11 | opts
12 | |> Macro.expand(__CALLER__)
13 | |> Keyword.get(:with, [:n2o])
14 |
15 | Enum.map(imports, fn mod ->
16 | if Code.ensure_compiled(mod) do
17 | upcased = Module.concat([String.upcase(to_string(mod))])
18 |
19 | quote do
20 | import unquote(upcased)
21 | alias unquote(mod), as: unquote(upcased)
22 | end
23 | else
24 | IO.warn(
25 | "🚨 Unknown module #{mod} was requested to be used by :n2o. Skipping."
26 | )
27 | end
28 | end)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/aes_gcm.ex:
--------------------------------------------------------------------------------
1 | defmodule AES.GCM do
2 | @aad "AES256GCM"
3 |
4 | def secret_key(), do: :application.get_env(:n2o, :secret, "ThisIsClassified")
5 |
6 | def depickle(hex) do
7 | try do
8 | cipher = :n2o_secret.unhex(hex)
9 | <> = cipher
10 | term = :crypto.crypto_one_time_aead(:aes_128_gcm,
11 | secret_key(), iv, bin, @aad, tag, false)
12 | :erlang.binary_to_term(term, [:safe])
13 | rescue
14 | _ -> ""
15 | end
16 | end
17 |
18 | def pickle(term) do
19 | bin = :erlang.term_to_binary(term)
20 | iv = :crypto.strong_rand_bytes(16)
21 |
22 | {cipher, tag} =
23 | :crypto.crypto_one_time_aead(:aes_128_gcm,
24 | secret_key(), iv, bin, @aad, true)
25 |
26 | bin = iv <> tag <> cipher
27 | :n2o_secret.hex(bin)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/ftp.ex:
--------------------------------------------------------------------------------
1 | defmodule N2O.FTP do
2 | require N2O
3 | require Kernel
4 |
5 | def root() do
6 | {:ok, cwd} = :file.get_cwd()
7 | :filename.join(cwd, :application.get_env(:n2o,:upload,:code.priv_dir(:n2o)))
8 | end
9 |
10 | def chunk(), do: 256 * 1024
11 |
12 | def fileName(N2O.ftp(filename: fileName)), do: fileName
13 |
14 | def event({:ftpInit, N2O.ftp(id: guid, status: "init", filename: fileName) = ftp, pid}) do
15 | IO.inspect("DEBUG 1")
16 | filePath = :filename.join(root(), fileName)
17 | :filelib.ensure_dir(filePath)
18 | Supervisor.start_link([], strategy: :one_for_one, name: FTP)
19 |
20 | try do :n2o_pi.stop(:ftp, guid) catch _,_ -> [] end
21 | initFtp = N2O.ftp(ftp, block: chunk(), offset: 0, data: <<>>)
22 | N2O.pi(
23 | module: FTP,
24 | table: :ftp,
25 | sup: FTP,
26 | state: {initFtp, []},
27 | timeout: 180000,
28 | name: guid
29 | )
30 | |> :n2o_pi.start()
31 | IO.inspect(pid, label: "PID")
32 | :erlang.send(pid, {:direct, {:ftp_init, initFtp}})
33 |
34 | apply(:nitro,:wire,["ftp_init('#{guid}', 0, #{chunk()}, '#{fileName}')"])
35 | end
36 |
37 | def event({:ftpSend, N2O.ftp(id: guid, status: "send") = ftp, pid}) do
38 | IO.inspect(ftp, label: "DEBUG 2")
39 | N2O.ftp(size: totalSize, offset: offset, block: block, filename: fileName) = ftp2 =
40 | try do
41 | :n2o_pi.send(:ftp, guid, {:send, ftp, pid})
42 | catch _ -> N2O.ftp(ftp, data: <<>>, block: 0)
43 | end
44 | IO.inspect(ftp2, label: "DEBUG 3")
45 | apply(:nitro,:wire,["ftp_send('#{guid}', #{totalSize}, #{offset}, #{block}, '#{fileName}')"])
46 | end
47 |
48 | def proc(:init, N2O.pi(state: {N2O.ftp() = ftp, _}) = pi) do
49 | IO.inspect(ftp, label: "N2O PI INIT")
50 | {:ok, N2O.pi(pi, state: {ftp, ping(100)})}
51 | end
52 |
53 | def proc({:check}, N2O.pi(state: {x, timer}) = pi) do
54 | :erlang.cancel_timer(timer)
55 | new_timer = ping(30000)
56 | {:noreply, N2O.pi(pi, state: {x, new_timer})}
57 | end
58 |
59 | def proc({:send, N2O.ftp(status: "send", data: data, block: block) = ftp, web_pid}, N2O.pi(name: guid, state: {N2O.ftp(filename: fileName, size: totalSize, offset: offset), timer}) = pi) when offset + block >= totalSize do
60 | filePath = :filename.join(root(), fileName)
61 | finishFtp = N2O.ftp(ftp, data: <<>>, offset: totalSize, filename: fileName, block: 0)
62 | case :file.write_file(filePath, :erlang.iolist_to_binary(data), [:append, :raw]) do
63 | :ok ->
64 | spawn(fn -> :erlang.send(web_pid, {:direct, {:ftp_finish, finishFtp}}) end)
65 | spawn(fn -> try do :n2o_pi.stop(:ftp, guid) catch _,_ -> [] end end)
66 | {:stop, :normal, finishFtp, N2O.pi(pi, state: {finishFtp, timer})}
67 | {:error, _} = x -> {:reply, x, pi}
68 | end
69 | end
70 |
71 | def proc({:send, N2O.ftp(status: "send", data: data, block: block) = ftp, _web_pid}, N2O.pi(state: {N2O.ftp(filename: fileName, offset: offset), timer}) = pi) do
72 | IO.inspect("N2O PI SEND")
73 | filePath = :filename.join(root(), fileName)
74 | nextFtp = N2O.ftp(ftp, data: <<>>, offset: offset + block, filename: fileName)
75 | case :file.write_file(filePath, :erlang.iolist_to_binary(data), [:append, :raw]) do
76 | :ok -> {:reply, nextFtp, N2O.pi(pi, state: {nextFtp, timer})}
77 | {:error, _} = x -> {:reply, x, pi}
78 | end
79 | end
80 |
81 | def proc(_, N2O.pi(state: {_, timer}) = pi), do: {:reply, :ok, N2O.pi(pi, state: {[], timer})}
82 |
83 | def ping(milliseconds), do: :erlang.send_after(milliseconds, self(), {:check})
84 |
85 | end
86 |
--------------------------------------------------------------------------------
/man/N2O.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | N2O
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | N2O
25 |
26 |
27 |
28 |
29 |
30 | STREAM
31 |
32 |
33 |
34 |
35 |
36 | CACHE
37 |
38 |
39 |
40 |
41 |
42 | ASYNC
43 |
44 |
45 |
46 |
47 |
48 | SESSION
49 |
50 |
51 |
52 |
53 |
54 | VNODE
55 |
56 |
57 |
58 |
59 |
60 | TIMER
61 |
62 |
63 |
64 |
65 |
66 | PROTO
67 |
68 |
69 |
70 |
71 |
72 | NITRO
73 |
74 |
75 |
76 |
77 |
78 | BPE
79 |
80 |
81 |
82 |
83 |
84 | ROSTER
85 |
86 |
87 |
88 |
89 |
90 | INIT
91 |
92 |
93 |
94 |
95 |
96 | MQ
97 |
98 |
99 |
100 |
101 |
102 | FORMAT
103 |
104 |
105 |
106 |
107 |
108 | LOG
109 |
110 |
111 |
112 |
113 |
114 | Services
115 |
116 |
117 | Loop
118 |
119 |
120 | Modules
121 |
122 |
123 | Public Modules
124 |
125 |
126 | Enabled Protocols
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------
/man/ftp.js.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | FTP.JS
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | FTP.JS
23 |
30 |
31 |
32 |
33 | FTP.JS
34 |
35 |
36 |
37 |
38 | INTRO
39 | The ftp.js
40 | module provides client FTP protocol implementation.
41 |
42 |
43 |
44 | API
45 | $file.do(response)
46 | The event handler interceptor of the incoming FTP message.
47 | $file.progress(offset,total)
48 | The event handler interceptor of the file upload progree.
49 | ftp.init(file)
50 | Initialization of File transfer, returns unique id generated for it.
51 | ftp.start(id)
52 | Initialization of File transfer by id.
53 | ftp.stop(id)
54 | Termination of File transfer by id.
55 | ftp.send(item,data)
56 | Sends binary FTP data over WebSocket.
57 |
58 |
67 |
68 |
69 | 2005—2020 © Synrc Research Center
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/man/heart.js.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | HEART.JS
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | HEART.JS
23 |
30 |
31 |
32 |
33 | HEART.JS
34 |
35 |
36 |
37 |
38 | INTRO
39 | The heart.js
40 | module provides client HEART protocol implementation.
41 | The size of heart.js is 1152 bytes.
42 |
43 |
44 |
45 | CONFIG
46 | Configuration consists of:
47 | 1) current transport;
48 | 2) array of available transports;
49 | 3) reconnection delay;
50 | 4) threshold of maximum reconnects.
51 |
52 |
53 | ct = 0;
54 | transports = [ $ws ];
55 | reconnectDelay = 1000;
56 | maxReconnects = 100;
57 |
58 |
59 |
60 |
61 | TRANSPORT
62 | Transport contains PING interval. On hearbeat ws.send('PING') is performed.
63 |
64 |
65 | $ws = { heart: true,
66 | interval: 5000,
67 | creator: function(url) { },
68 | onheartbeat: function() { } }
69 |
70 |
71 |
72 |
73 | CONNECTION
74 |
75 |
76 | $conn = { onopen: nop,
77 | onmessage: nop,
78 | onclose: nop,
79 | onconnect: nop,
80 | send: function(data) { },
81 | close: function() { } }
82 |
83 |
84 |
85 |
94 |
95 |
96 | 2005—2020 © Synrc Research Center
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/man/ieee754.js.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | IEEE754.JS
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | IEEE754.JS
23 |
30 |
31 |
32 |
33 | IEEE754.JS
34 |
35 |
36 |
37 |
38 | INTRO
39 | The ieee754.js
40 | module provides IEEE-754 binary encoder/decoder.
41 | This size of ieee754.js is 1404 bytes.
42 |
43 |
44 |
45 | API
46 | read_Float(buffer, offset, isLE, mantis, len)
47 | Reads raw IEEE-754 fload from the buffer.
48 | write_Float(buffer, value, offset, isLE, mantis, len)
49 | Writes raw IEEE-754 fload into the buffer.
50 |
51 |
58 |
59 |
60 | 2005—2020 © Synrc Research Center
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/man/mq.js.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | MQ.JS
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | MQ.JS
23 |
30 |
31 |
32 |
33 | MQ.JS
34 |
35 |
36 |
37 |
38 | INTRO
39 | The mq.js
40 | module provides client MQTT protocol connectivity.
41 |
42 |
43 |
50 |
51 |
52 | 2005—2020 © Synrc Research Center
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/man/n2o.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | N2O
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | API
23 |
30 |
31 |
32 |
33 | N2O
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o
40 | defines the way you create, configure and run
41 | arbitrary applications and protocols inside some hosts, into
42 | which N2O can be injected, such as
43 | cowboy
44 | and emqttd .
45 | Each application can spawn its instance in its way like
46 | web pages spawn WebSocket connections, workflow engines
47 | spawn business processes, and chat applications spawns roster
48 | and chatroom processes. With N2O everything is managed by protocols.
49 |
50 | N2O shipped to work in two modes:
51 | 1) inside n2o_mqtt workers;
52 | 2) inside cowboy processes, implemented in n2o_stream .
53 | In the first case, the MQTT server used between clients and server workers.
54 | In the second case, no more Erlang processes introduced except clients.
55 | You can create your configuration of N2O processing loop.
56 |
57 | The N2O itself is an embeddable protocol loop in n2o_proto .
58 | However, besides that, it handles cache and sessions
59 | along with flexible n2o_pi processes with no ownership restriction.
60 | It also introduces AES/CBC—128 pickling and BERT/JSON encoder.
61 |
62 |
63 | TYPES
64 |
65 |
66 | -type formatter() :: binary | json | bert | text | default | atom().
67 | -type response() :: { formatter(), binary() }.
68 |
69 |
70 |
71 | Listing 1. Erlang/OTP records
72 |
73 | #ok { data = [] :: term() }.
74 | #error { data = [] :: term() }.
75 |
76 |
77 |
78 | Listing 2. N2O Protocol
79 |
80 | #reply { resp = [] :: [] | response(),
81 | req = [] :: [] | term(),
82 | state = [] :: [] | term() }.
83 |
84 | #unknown { data = [] :: [] | binary(),
85 | req = [] :: [] | term(),
86 | state = [] :: [] | term() }.
87 |
88 |
89 |
90 |
91 | Listing 3. N2O State
92 |
93 | #cx { session = [] :: [] | binary(),
94 | formatter = bert :: bert | json,
95 | actions = [] :: list(tuple()),
96 | state = [] :: [] | term(),
97 | module = [] :: [] | atom(),
98 | lang = [] :: [] | atom(),
99 | path = [] :: [] | binary(),
100 | node = [] :: [] | atom(),
101 | pid = [] :: [] | pid(),
102 | vsn = [] :: [] | integer() }).
103 |
104 |
105 |
106 |
107 | PROTOCOL
108 | While all application protocols in the system are desired
109 | to have single effect environment or same error handling path,
110 | n2o defines a single protocol loop for all applications
111 | as stack of protocols.
112 | In core bundle n2o is shipped with NITRO and FTP protocols
113 | which allows you to create real-time web applications with
114 | binary-based protocols, also providing robust and performant
115 | upload client and file transfer protocol. For building
116 | web-based NITRO applications, you need to include nitro dependency.
117 | info(term(),term(),#cx{}) -> #reply{} | #unknown{}.
118 | The info/3 is an N2O protocol callback that to be called
119 | on each incoming request.
120 |
121 |
122 | RPC MQTT
123 | N2O provides RPC over MQ mechanism for MQTT devices.
124 | N2O spawns a set of n2o_mqtt workers
125 | as n2o_pi processes that listen to
126 | events topic. The responses send to actions topic, which is
127 | subscribed automatically on MQTT session init.
128 |
129 | Listing 5. MQTT RPC Topics
130 |
131 | actions/:vsn/:module/:client
132 | events/:vsn/:node/:module/:client
133 |
134 |
135 |
136 |
137 | RPC WebSocket
138 | In pure WebSocket case, N2O implements n2o_stream
139 | as cowboy module supporting binary and text messages.
140 |
141 | Listing 6. Cowboy stream protocol
142 |
143 | #binary { data :: binary() }.
144 | #text { data :: binary() }.
145 |
146 |
147 |
148 |
149 | EXAMPLE
150 | Here is an example of overriding INIT protocol.
151 |
152 | Listing 7. Custom INIT Protocol
153 |
154 | -module(custom_init).
155 | -include("n2o.hrl").
156 | -export([info/3]).
157 |
158 | info({text,<<"N2O,",Pickle/binary>>}, Req, State) ->
159 | {'Token',Token} = n2o_session:authenticate([],Pickle),
160 | Sid = case n2o:depickle(Token) of {{S,_},_} -> S; X -> X end,
161 | New = State#cx{session = Sid},
162 | {reply,{bert,{io,<<"console.log('connected')">>,
163 | {'Token',Token}}}, Req, New};
164 |
165 | info(Message,Req,State) -> {unknown,Message,Req,State}.
166 |
167 |
168 |
169 |
170 | CONFIG
171 | Just put protocol implementation module name to protocol option in sys.config.
172 |
173 |
174 | [{n2o,[{cache,n2o},
175 | {upload,"priv/static"},
176 | {mq,n2o_syn},
177 | {ttl,900},
178 | {timer,{0,1,0}}
179 | {tables,[cookies,file,caching,ring,async]},
180 | {hmac,sha256},
181 | {filename,n2o_ftp},
182 | {formatter,n2o_bert},
183 | {session,n2o_session},
184 | {pickler,n2o_secret},
185 | {protocols,[n2o_ftp,n2o_nitro]},
186 | {nitro_prolongate,false},
187 | {filter,{n2o_proto,push}},
188 | {origin,<<"*">>},
189 | {timer,{0,10,0}}]}].
190 |
191 |
192 | N2O is the facade of the following services: cache, MQ, message formatting,
193 | sessions, pickling and protocol loops. The other part of N2O is n2o_pi module
194 | for spawning supervised application processes to use N2O API. In this simple
195 | configuration, you may set any implementation for any service.
196 |
197 |
198 | The following configurable services are publicly available in n2o module:
199 |
200 |
201 | CACHE
202 | Cache is a fast expirable memory store. Just put values onto keys using
203 | these functions and system timer will clear expired entries eventually.
204 | You can select caching module implementation by setting cache n2o parameter
205 | to the module name. Default n2o cache implementation turns each ets store
206 | into expirable.
207 |
208 | Sets a Value with a given TTL.
209 |
210 | cache(Tab, Key, Value, Till) -> term().
211 |
212 |
213 |
214 | Gets a Value.
215 |
216 | cache(Tab, Key) -> term().
217 |
218 |
219 |
220 |
221 | MQ
222 | The minimal requirement for any framework is the pub/sub API.
223 | N2O provides selectable API through mq environment parameter.
224 |
225 |
226 | reg(term()) -> term().
227 |
228 |
229 | Subscribe a current client to a transient topic. In particular
230 | implementation, the semantics could differ. In MQTT you can
231 | subscribe offline/online clients to any persistent topic. Also in MQTT
232 | this function subscribes MQTT client not an Erlang process.
233 |
234 |
235 | unreg(term()) -> term().
236 |
237 |
238 | Unsubscribe a current client from a transient topic.
239 | In MQTT we remove the subscription from the persistent database.
240 |
241 |
242 | send(term(), term()) -> term().
243 |
244 |
245 | Publish a message to a topic. In MQTT if clients are offline,
246 | they will receive offline messages from the in-flight storage
247 | once they become online.
248 |
249 |
250 | FORMAT
251 | You specify the formatter in the protocol return message. E.g:
252 |
253 |
254 | info({Code}, Req, State) ->
255 | {reply,{bert,{io,nitro:jse(Code),<<>>}}, Req, State};
256 |
257 |
258 |
259 | Serializes a record.
260 |
261 | encode(record()) -> binary().
262 |
263 |
264 |
265 | Deserializes a record.
266 |
267 | decode(binary()) -> record().
268 |
269 |
270 |
271 | Here is an example of n2o_bert formatter implementation.
272 |
273 | encode(Erl) -> term_to_binary(Erl).
274 | decode(Bin) -> binary_to_term(Bin,[safe]).
275 |
276 |
277 |
278 |
279 | SESSION
280 | Sessions are stored in issued tokens encrypted with AES/GCM-256.
281 | All session variables are cached in ETS table in the default
282 | implementation n2o_session .
283 |
284 |
285 | Sets a value to session variable.
286 |
287 | session(Key, Value) -> term().
288 |
289 |
290 |
291 |
292 | Listing 8. Sessions
293 |
294 | 1> rr(n2o).
295 | [bin,client,cx,direct,ev,flush,ftp,ftpack,handler,
296 | mqtt_client,mqtt_message,pickle,server]
297 | 2> put(context,#cx{}).
298 | undefined
299 | 3> n2o:session(user,maxim).
300 | maxim
301 | 4> ets:tab2list(cookies).
302 | [{{[],user},{63710014344,"maxim"}},
303 | {{<<"5842b7e749a8cf44c920">>,auth},{63710014069,[]}]
304 |
305 |
306 |
307 | Gets a value of session variable.
308 |
309 | session(Key) -> term().
310 |
311 |
312 |
313 |
314 | PICKLE
315 |
316 | Custom Erlang term serialization.
317 |
318 | pickle(term()) -> binary().
319 |
320 |
321 |
322 |
323 | Custom Erlang term deserialization.
324 |
325 | depickle(binary()) -> term().
326 |
327 |
328 |
329 |
338 |
339 |
340 | 2005—2020 © Synrc Research Center
341 |
342 |
343 |
344 |
--------------------------------------------------------------------------------
/man/n2o.js.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | N2O.JS
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | N2O.JS
23 |
30 |
31 |
32 |
33 | N2O.JS
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o.js
40 | module provides N2O protocol loop at JavaScript side.
41 | The size of n2o.js is 2120 bytes.
42 |
43 |
44 |
45 | API
46 | N2O_start()
47 | This function creates a WebSocket raw connection to N2O WS and
48 | sends <<"N2O,",Token/binary>> text packet
49 | which invokes NITRO protocol page initialization.
50 | token()
51 | Returns current encrypted AES/CBC-128 session token.
52 | ws.send(bert)
53 | Sends raw binary to WebSocket. Use bert.js JavaScript encoder.
54 | ws.send(enc(tuple(atom('direct'),atom('test'))))
55 | $bert.protos
56 | List of supported client loopers for particular BERT messages.
57 |
58 |
66 |
67 |
68 | 2005—2020 © Synrc Research Center
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/man/n2o_auth.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | AUTH
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | AUTH
23 |
30 |
31 |
32 |
33 | AUTH
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o_auth
40 | module provides EMQ auth hook that perform
41 | client's autosubscribe to client topic.
42 |
43 |
44 | CALLBACK
45 |
46 |
47 | check(#mqtt_client{},binary(),term()) -> ok | ignore.
48 |
49 |
50 | The default implementation performs client topic autosubscribe at server side.
51 |
52 |
53 | check(#mqtt_client{client_id = ClientId, username = PageModule,
54 | client_pid = ClientPid,
55 | ws_initial_headers = _Headers},
56 | Password, Listeners) ->
57 | ClientId = ensure_id(Client),
58 | case ClientId of
59 | <<"emqttd_", _/binary>> ->
60 | emqttd_client:subscribe(ClientPid,
61 | [{n2o:to_binary(["actions/1/",PageModule,"/",ClientId]), 2}]),
62 | ignore;
63 | _ -> ignore
64 | end.
65 |
66 |
67 |
68 |
75 |
76 |
77 | 2005—2020 © Synrc Research Center
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/man/n2o_bert.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | BERT
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | BERT
23 |
30 |
31 |
32 |
33 | BERT
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o_bert
40 | module provides BERT encoder/decoder.
41 |
42 |
43 | You may also want to read:
44 | n2o_json ,
45 | n2o .
46 |
47 |
48 |
49 |
50 | 2005—2020 © Synrc Research Center
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/man/n2o_cowboy.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | COWBOY
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | COWBOY
23 |
30 |
31 |
32 |
33 | COWBOY
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o_cowboy
40 | module provides COWBOY API interface connectivity.
41 |
42 |
48 |
49 |
50 | 2005—2020 © Synrc Research Center
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/man/n2o_ftp.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | FTP
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | FTP
23 |
30 |
31 |
32 |
33 | FTP
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o_ftp
40 | module provides file upload protocol implementation.
41 |
42 |
43 | FTP
44 |
45 |
46 |
47 | -record(ftp, { id = [] :: [] | binary(),
48 | sid = [] :: [] | binary(),
49 | filename = [] :: [] | binary(),
50 | meta = [] :: any(),
51 | size = [] :: [] | integer(),
52 | offset = [] :: [] | integer(),
53 | block = [] :: [] | integer(),
54 | data = [] :: [] | binary(),
55 | status = [] :: [] | binary() }).
56 |
57 |
58 |
59 |
60 |
67 |
68 |
69 | 2005—2020 © Synrc Research Center
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/man/n2o_gproc.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | GPROC
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | GPROC
23 |
30 |
31 |
32 |
33 | GPROC
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o_gproc
40 | module provides GPROC MQ backend.
41 |
42 |
48 |
49 |
50 | 2005—2020 © Synrc Research Center
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/man/n2o_heart.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | HEART
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | HEART
23 |
30 |
31 |
32 |
33 | HEART
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o_heart
40 | module provides HEART protocol implementation.
41 |
42 |
43 | PING
44 |
45 |
46 | Input Messages
47 |
48 | <<"PING">>
49 | <<>>
50 |
51 |
52 |
53 |
54 | Output Messages
55 |
56 | <<"PONG">>
57 | <<>>
58 |
59 |
60 |
61 |
62 |
68 |
69 |
70 | 2005—2020 © Synrc Research Center
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/man/n2o_json.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | JSON
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | JSON
23 |
30 |
31 |
32 |
33 | JSON
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o_json
40 | module provides JSON encoder/decoder.
41 |
42 |
43 | You may also want to read:
44 | n2o_bert ,
45 | n2o .
46 |
47 |
48 |
49 |
50 | 2005—2020 © Synrc Research Center
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/man/n2o_mqtt.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | MQTT
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | MQTT
23 |
30 |
31 |
32 |
33 | MQTT
34 |
35 |
36 |
37 |
38 | INTRO
39 | n2o_mqtt
40 | is an instance of n2o_pi process. In N2O
41 | number of such instanses is usually equals to the number of cores.
42 | There is two styles of embedding N2O: one is to perform N2O protocol loop inside
43 | connection process (as in cowboy version); second is to spawn ring of n2o_pi
44 | processes and handle all requests inside these processes (as in MQTT version). n2o_mqtt is
45 | the protocol of such virtual node processes.
46 |
47 |
48 | PROTOCOL
49 | On init n2o_mqtt makes connection to MQTT server.
50 | On connection established in #mqttc/2 it subscribes to events topic.
51 |
52 |
53 | events/+/:name/#
54 |
55 |
56 | After supscription all messages that are being sent to events topic are
57 | passed as #publish/2 where n2o_proto:info/3 protocol loop is
58 | embedded as handler to that message.
59 |
60 |
61 | #publish { topic = [] :: binary(),
62 | message = [] :: binary()}.
63 |
64 | #mqttc { client :: pid(),
65 | status :: connected }.
66 |
67 |
68 | All the results go to actions topic:
69 |
70 |
71 | actions/:vsn/:module/:client
72 |
73 |
74 |
75 |
76 | CONFIG
77 | Service workers start MQTT clients with following config:
78 |
79 |
80 | [{mqtt,[{host, "127.0.0.1"},
81 | {client_id, Gnerated},
82 | {clean_sess, false},
83 | {logger, {console, error}},
84 | {reconnect, 5}]}].
85 |
86 |
87 |
88 |
95 |
96 |
97 | 2005—2020 © Synrc Research Center
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/man/n2o_pi.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | PI
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | PI
23 |
30 |
31 |
32 |
33 | PI
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o_pi
40 | module dedicated for creating and tracking supervised
41 | processes across all applications, using any ETS tables. Any supervised process in N2O
42 | is created using n2o_pi, such as: ring vnodes, timers, authorization,
43 | web page processes, test processes and other services. Loop process inside info/2 protocol handlers
44 | should spawn new async processes proc/2 in case of time consuming operations,
45 | because protocol handler is a critical path and it should be handled as soon as possible.
46 |
47 |
48 | CALLBACK
49 |
50 |
51 | proc(term(),#pi{}) -> #ok{} | #reply{}.
52 |
53 |
54 | The proc/2 is a callback that will be called on each
55 | gen_server 's calls: handle_call ,
56 | handle_cast and handle_info , its init
57 | and terminate . It returns either #ok as initial state of the process (which is the #pi{} too)
58 | or its response to gen_server:call/2 with new state included in #reply.
59 |
60 |
61 | EXAMPLE
62 | Here is literal implementation of N2O Timer which invalidates
63 | the caching table used for session variables.
64 |
65 | Listing 1. Invalidate Cache by Timer
66 |
67 | proc(init,#pi{}=Async) ->
68 | {ok,Async#pi{state=timer(ping())}};
69 |
70 | proc({timer,ping},#pi{state=Timer}=Async) ->
71 | erlang:cancel_timer(Timer),
72 | io:format("n2o Timer: ~p\r~n",[ping]),
73 | n2o:invalidate_cache(caching),
74 | {noreply,Async#pi{state=timer(ping())}}.
75 |
76 | timer(Diff) ->
77 | {X,Y,Z} = Diff,
78 | erlang:send_after(1000*(Z+60*Y+60*60*X),self(),{timer,ping}).
79 |
80 | ping() ->
81 | application:get_env(n2o,timer,{0,1,0}).
82 |
83 |
84 |
85 | Listing 1. Invalidate Cache by Timer
86 |
87 | > n2o_pi:start(#pi{ module = n2o,
88 | table = caching,
89 | sup = n2o,
90 | state = [],
91 | name = "timer"}).
92 |
93 |
94 | Main purpose of n2o_pi is to create such processes from
95 | single proc/2 function and track pid in ETS table which is specified during
96 | process #pi{} initialization.
97 |
98 | Listing 2. Understanding n2o_pi
99 |
100 | 1> supervisor:which_children(n2o).
101 | [{{ring,4},<0.1661.0>,worker,[n2o_mqtt]},
102 | {{ring,3},<0.1655.0>,worker,[n2o_mqtt]},
103 | {{ring,2},<0.1653.0>,worker,[n2o_mqtt]},
104 | {{ring,1},<0.1651.0v,worker,[n2o_mqtt]},
105 | {{caching,"timer"},<0.1604.0>,worker,[n2o]}]
106 |
107 | 2> ets:tab2list(ring).
108 | [{{ring,4},infinity,<0.1661.0>},
109 | {{ring,1},infinity,<0.1651.0>},
110 | {{ring,2},infinity,<0.1653.0>},
111 | {{ring,3},infinity,<0.1655.0>}]
112 |
113 | 3> ets:tab2list(caching).
114 | [{{caching,"timer"},infinity,<0.1604.0>}]
115 |
116 | 4> n2o_pi:cast(caching,"/timer",{timer,ping}).
117 | n2o Timer: ping
118 | ok
119 |
120 | 5> n2o_pi:pid(caching,"/timer").
121 | <0.1604.0>
122 |
123 |
124 |
125 |
126 | RECORDS
127 | Each process is driven by its protocol which in fact a sum of protocol messages.
128 | Though n2o_pi as being generic don't limit the protocol messages,
129 | however it defines the type of process state, the #pi{} record.
130 |
131 | Listing 3. Erlang/OTP Records
132 |
133 | #ok { code = [] :: [] | #pi{} }.
134 |
135 | #error { code = [] :: [] | term() }.
136 |
137 | #reply { data = [] :: [] | term() ,
138 | code = [] :: [] | #pi{} }.
139 |
140 |
141 | According to N2O agreement each protocol message field should include [] in its type as default nil.
142 |
143 | Listing 4. N2O Records
144 |
145 | -record(pi, { name :: atom(),
146 | table :: ets:tid(),
147 | sup :: atom(),
148 | module :: atom(),
149 | state :: term() }).
150 |
151 |
152 |
153 | name — process name, key in supervised chain.
154 | module — the module name where proc/2 is placed.
155 | table — ETS table name where cached pids are stored.
156 | sup — the application, where supervised processes will be created.
157 | state — the state of the running supervised process.
158 |
159 |
160 |
161 | API
162 |
163 |
164 | start(#pi{}) -> {pid(),term()} | #error{}.
165 |
166 |
167 | Spawns proc/2 function inside a process under supervision.
168 |
169 |
170 | stop(Class,Name) -> #pi{} | #error{}.
171 |
172 |
173 | Kills the process and remove from supervision.
174 |
175 |
176 | restart(Class,Name) -> {pid(),term()} | #error{} | #pi{}.
177 |
178 |
179 | Tries to stop the process. On success it starts the new one, else return error.
180 |
181 |
182 | send(Class,Name,term()) -> term().
183 |
184 |
185 | Sends gen_call message to process, taken from Class table with Name key.
186 | Returns the response from gen_server:call .
187 |
188 |
189 | cast(Class,Name,term()) -> term().
190 |
191 |
192 | Sends gen_cast message to process, taken from Class table with Name key.
193 |
194 |
195 | pid(Class,Name) -> pid().
196 |
197 |
198 | Returns pid that was stored during process initialization in Class table with Name key.
199 |
200 | Listing 5. gen_server compatibility.
201 |
202 | 1> n2o_pi:pid(caching,"/timer")
203 | ! {timer,ping}.
204 | n2o Timer: ping
205 | {timer,ping}
206 |
207 | 2> gen_server:cast(
208 | n2o_pi:pid(caching,"/timer"),
209 | {timer,ping}).
210 | n2o Timer: ping
211 | ok
212 |
213 |
214 |
215 |
222 |
223 |
224 | 2005—2020 © Synrc Research Center
225 |
226 |
227 |
228 |
--------------------------------------------------------------------------------
/man/n2o_proto.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | PROTO
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | PROTO
23 |
30 |
31 |
32 |
33 | PROTO
34 |
35 |
36 |
37 |
38 | INTRO
39 | N2O as an embeddable framework provides two exports for loop entrance.
40 |
41 |
42 | API
43 | The n2o_proto
44 | protocol loop is compatible with cowboy and emqttd versions of N2O.
45 | The main purpose of this API is to embed N2O into your functional endpoint.
46 | In other cases it's called automatically by configuration.
47 |
48 |
49 |
50 | stream({ text | binary, binary() }, term(), term()) -> #reply{}.
51 |
52 |
53 | The stream function is dedicated for cowboy version of n2o.
54 |
55 |
56 |
57 | info(term(), term(), term()) -> #reply{}.
58 |
59 |
60 | The info function is dedicated for emqttd version of n2o.
61 |
62 |
63 | push(term(), term(), term(), term(), term()) -> #reply{}.
64 |
65 |
66 | The raw protocol chain processing without using filtering.
67 | You can intercept all messages from stream/3 and info/3 by
68 | setting filter function in sys.config.
69 |
70 |
71 | CONFIG
72 |
73 |
74 | [{n2o,[{protocols,[n2o_nitro]},
75 | {filter,{n2o_proto,push}}]}].
76 |
77 |
78 |
79 |
80 | IMPLEMENTATION
81 |
82 |
83 | nop(R,S) -> {reply,{binary,<<>>},R,S}.
84 | reply(M,R,S) -> {reply,M,R,S}.
85 | push(_,R,S,[],_) -> nop(R,S);
86 | push(M,R,S,[H|T],Acc) ->
87 | case H:info(M,R,S) of
88 | {unknown,_,_,_} -> push(M,R,S,T,Acc);
89 | {reply,M1,R1,S1} -> reply(M1,R1,S1);
90 | A -> push(M,R,S,T,[A|Acc]) end.
91 |
92 |
93 |
94 |
95 | This module may refer to:
96 | n2o ,
97 | n2o_pi .
98 |
99 |
100 |
101 |
102 | 2005—2020 © Synrc Research Center
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/man/n2o_ring.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | RING
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | RING
23 |
30 |
31 |
32 |
33 | RING
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o_ring
40 | module provides hash ring for virtual nodes.
41 |
42 |
49 |
50 |
51 | 2005—2020 © Synrc Research Center
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/man/n2o_secret.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | SECRET
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | SECRET
23 |
30 |
31 |
32 |
33 | SECRET
34 |
35 |
36 |
37 |
38 | INTRO
39 | The AES.GCM
40 | module provides AES/GCM-256 encoder/decoder.
41 |
42 |
43 | API
44 |
45 | Encode the term
46 |
47 | pickle(term()) -> binary().
48 |
49 |
50 |
51 | Decode the term
52 |
53 | depickle(binary()) -> term().
54 |
55 |
56 |
57 | Get HEX of first 10 bytes of HMAC/SHA256 signed seed with a secret
58 |
59 | sid(term()) -> binary().
60 |
61 |
62 |
63 |
64 | You may also want to read:
65 | n2o_bert
66 |
67 |
68 |
69 |
70 | 2005—2020 © Synrc Research Center
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/man/n2o_session.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | SESSION
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | SESSION
23 |
30 |
31 |
32 |
33 | SESSION
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o_session
40 | module provides session variables protected by authorization token.
41 |
42 |
43 | > ets:tab2list(cookies).
44 | [{{<<"05dcf467c79071008bc4">>,user},{63710034516,"maxim"}},
45 | {{<<"05dcf467c79071008bc4">>,room},{63710034516,"n2o"}},
46 | {{<<"05dcf467c79071008bc4">>,auth},{63710033891,[]}}]
47 |
48 |
49 |
50 |
51 | TYPES
52 | Session token represents tuple cached in ETS.
53 | It consists of session id, key, expiration time and value.
54 | Session token used both for auth and for session variables.
55 |
56 |
57 | -type expires() :: integer().
58 | -type session_sid() :: binary().
59 | -type session_key() :: { session_sid() , term() }.
60 | -type session_val() :: { expires() , term() }.
61 | -type session() :: { session_key() , session_val() }.
62 |
63 |
64 | Session token encoded with pickler tagged as 'Token' tuple.
65 |
66 |
67 | -type token() :: { 'Token', binary() }.
68 |
69 |
70 |
71 |
72 | API
73 |
74 |
75 | authenticate([], binary()) -> token().
76 |
77 |
78 | The protocol is following. If session is expired or session
79 | token is invalid then new session token issued. If session token
80 | is not expired and valid then two cases could happen:
81 | 1) if nitro_prolongate n2o parameter is true,
82 | then new expiration value is updated for current session, while
83 | session sid remains unchanged;
84 | 2) if nitro_prolongate is false then full session token remains unchanged.
85 |
86 | Get session variable from session storage
87 |
88 | get_value(binary(), term(), term()) -> term().
89 |
90 |
91 |
92 |
93 | Set session variable to session storage
94 |
95 | set_value(binary(), term(), term()) -> term().
96 |
97 |
98 |
99 |
100 | Get current sessions backend. n2o_session by default
101 |
102 | storage() -> atom().
103 |
104 |
105 |
106 |
107 | Get nitro_prolongate n2o parameter. false by default
108 |
109 | prolongate() -> boolean().
110 |
111 |
112 |
113 |
114 | BACKEND
115 |
116 | Clear all session variables by session sid
117 |
118 | clear(binary()) -> ok.
119 |
120 |
121 |
122 |
123 | Update session
124 |
125 | update(session()) -> ok.
126 |
127 |
128 |
129 |
130 | Remove session by key
131 |
132 | delete(session_key()) -> ok.
133 |
134 |
135 |
136 |
137 | Lookup session by key
138 |
139 | lookup(session_key()) -> ok.
140 |
141 |
142 |
143 |
144 | Remove all expired variables across all sessions
145 |
146 | invalidate_sessions() -> ok.
147 |
148 |
149 |
150 |
151 | You may also want to read:
152 | n2o_nitro ,
153 | n2o .
154 |
155 |
156 |
157 |
158 | 2005—2020 © Synrc Research Center
159 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/man/n2o_static.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | STATIC
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | STATIC
23 |
30 |
31 |
32 |
33 | STATIC
34 |
35 |
36 |
37 |
38 | INTRO
39 | The STATIC module.
40 |
41 |
42 | This module may refer to:
43 | MAN_MODULES
44 |
45 |
46 |
47 |
48 | 2005—2020 © Synrc Research Center
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/man/n2o_syn.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | SYN
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | SYN
23 |
30 |
31 |
32 |
33 | SYN
34 |
35 |
36 |
37 |
38 | INTRO
39 | The n2o_syn
40 | module provides SYN MQ backend.
41 |
42 |
48 |
49 |
50 | 2005—2020 © Synrc Research Center
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/man/n2o_ws.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | WS
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | WS
23 |
30 |
31 |
32 |
33 | WS
34 |
35 |
36 |
37 |
38 | INTRO
39 | The WS module.
40 |
41 |
42 | This module may refer to:
43 | MAN_MODULES
44 |
45 |
46 |
47 |
48 | 2005—2020 © Synrc Research Center
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/man/utf8.js.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | UTF8.JS
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | DEV
21 | N2O
22 | UTF8.JS
23 |
30 |
31 |
32 |
33 | UTF8.JS
34 |
35 |
36 |
37 |
38 | INTRO
39 | The utf8.js
40 | module provides UTF-8 encoder/decoder.
41 | The size of utf8.js is 227 bytes.
42 |
43 |
44 |
45 | API
46 | utf8_enc(x)
47 | Performs UTF-8 encoding
48 | utf8_dec(x)
49 | Performs UTF-8 decoding
50 | utf8_arr(x)
51 | Returns UTF-8 encoded ArrayBuffer .
52 |
53 |
61 |
62 |
63 | 2005—2020 © Synrc Research Center
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/man/zlib.js.htm:
--------------------------------------------------------------------------------
1 |
2 | FTP.JS
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | DEV
12 | N2O
13 | ZLIB.JS
14 |
21 |
22 |
23 | ZLIB.JS
24 |
25 |
26 |
27 |
28 | INTRO
29 |
30 | The module zlib.js
31 | provides zlib inflation algorithm (9KB).
32 |
33 |
34 |
35 |
47 |
48 |
49 | 2005—2019 © Synrc Research Center
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule N2O.Mixfile do
2 | use Mix.Project
3 | def deps, do: [{:ex_doc, ">= 0.0.0", only: :dev}]
4 | def application(), do:
5 | [
6 | mod: {:n2o, []},
7 | extra_applications: [:crypto, :xmerl]
8 | ]
9 |
10 | def project do
11 | [
12 | app: :n2o,
13 | version: "11.9.6",
14 | description: "N2O MQTT TCP WebSocket",
15 | package: package(),
16 | deps: deps()
17 | ]
18 | end
19 |
20 | def package do
21 | [
22 | files: ~w(doc include man priv lib src test mix.exs rebar.config LICENSE),
23 | licenses: ["ISC"],
24 | links: %{"GitHub" => "https://github.com/synrc/n2o"}
25 | ]
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/priv/bert.js:
--------------------------------------------------------------------------------
1 |
2 | // API
3 |
4 | function nil() { return { t: 106, v: [] }; }
5 | function tuple() { return { t: 104, v: Array.apply(null, arguments) }; }
6 | function list() { return { t: 108, v: Array.apply(null, arguments) }; }
7 | function map() { return { t: 116, v: Array.apply(null, arguments) }; }
8 | function atom(o) { return { t: 100, v: utf8_enc(o) }; }
9 | function string(o) { return { t: 107, v: utf8_enc(o) }; }
10 | function float(o) { return { t: 70, v: o }; }
11 | function number(o) {
12 | var s, isInteger = (o % 1 === 0);
13 | if (isInteger && o >= 0 && o < 256) { return { t: 97, v: o }; }
14 | if (isInteger && o >= -134217728 && o <= 134217727) { return {t: 98, v: o}; }
15 | return {t: 110, v: o}; }
16 | function bin(o) {
17 | return { t: 109, v: o instanceof ArrayBuffer ? new Uint8Array(o) :
18 | o instanceof Uint8Array ? o : utf8_enc(o) }; }
19 |
20 | // ENCODER
21 |
22 | function enc(o) { return fl([131, ein(o)]); }
23 | function ein(o) { return Array.isArray(o) ? en_108({ t: 108, v: o }) : eval('en_' + o.t)(o); }
24 | function en_undefined(o) { return [106]; }
25 | function en_70(o) {
26 | var x = Array(8).fill(0).flat();
27 | write_Float(x,o.v,0,false,52,8);
28 | return [70].concat(x);
29 | }
30 | function en_97(o) { return [97, o.v]; }
31 | function en_98(o) { return [98, o.v >>> 24, (o.v >>> 16) & 255, (o.v >>> 8) & 255, o.v & 255]; }
32 | function en_99(o) {
33 | var obj = o.v.toExponential(20),
34 | match = /([^e]+)(e[+-])(\d+)/.exec(obj),
35 | exponentialPart = match[3].length == 1 ? "0" + match[3] : match[3],
36 | num = Array.from(bin(match[1] + match[2] + exponentialPart).v);
37 | return [o.t].concat(num).concat(Array(31 - num.length).fill(0).flat());
38 | }
39 | function en_106(o) { return [106]; }
40 | function en_115(o) { return [115, o.v.length, ar(o)]; }
41 | function en_119(o) { return [119, ar(o).length, ar(o)]; }
42 | function en_118(o) { return [118, ar(o).length >>> 8, ar(o).length & 255, ar(o)]; }
43 | function en_100(o) { return [100, o.v.length >>> 8, o.v.length & 255, ar(o)]; }
44 | function en_107(o) { return [107, o.v.length >>> 8, o.v.length & 255, ar(o)]; }
45 | function en_104(o) {
46 | var l = o.v.length, r = [];
47 | for (var i = 0; i < l; i++)r[i] = ein(o.v[i]);
48 | return [104, l, r];
49 | }
50 | function unilen(o) {
51 | return (o.v instanceof ArrayBuffer || o.v instanceof Uint8Array) ? o.v.byteLength :
52 | (new TextEncoder().encode(o.v)).byteLength;
53 | }
54 |
55 | function en_109(o) {
56 | var l = unilen(o);
57 | return [109, l >>> 24, (l >>> 16) & 255, (l >>> 8) & 255, l & 255, ar(o)];
58 | }
59 | function en_108(o) {
60 | var l = o.v.length, r = [];
61 | for (var i = 0; i < l; i++)r.push(ein(o.v[i]));
62 | return o.v.length == 0 ? [106] :
63 | [108, l >>> 24, (l >>> 16) & 255, (l >>> 8) & 255, l & 255, r, 106];
64 | }
65 | function en_116(o) {
66 | var l=o.v.length,x=[],r = [];
67 | for (var i = 0; i < l; i++) r.push([ein(o.v[i].k),ein(o.v[i].v)]);
68 | x = [116, l >>> 24, (l >>> 16) & 255, (l >>> 8) & 255, l & 255];
69 | return o.v.length == 0 ? x : [x,r];
70 | }
71 | function en_110(o) {
72 | var s=int_to_bytes(o.v); return [110,s.length,(o.v<0)?1:0].concat(s);
73 | }
74 |
75 | // DECODER
76 |
77 | function nop(b) { return []; };
78 | function big(b) {
79 | var sk = b == 1 ? sx.getUint8(ix++) : sx.getInt32((a = ix, ix += 4, a));
80 | var ret = 0, sig = sx.getUint8(ix++), count = sk;
81 | while (count-- > 0) { ret = 256 * ret + sx.getUint8(ix + count); }
82 | ix += sk; return ret * (sig == 0 ? 1 : -1);
83 | }
84 | function int(b) {
85 | return b == 1 ? sx.getUint8(ix++) : sx.getInt32((a = ix, ix += 4, a));
86 | }
87 | function dec(d) {
88 | sx = new DataView(d); ix = 0;
89 | if (sx.getUint8(ix++) !== 131) throw ("BERT?");
90 | return din();
91 | }
92 | function str(b) {
93 | var dv, sz = (b==2?sx.getUint16(ix):(b==1?sx.getUint8(ix):sx.getUint32(ix)));
94 | ix += b; var r = sx.buffer.slice(ix, ix += sz);
95 | return utf8_arr(r);
96 | }
97 | function run(b) {
98 | var sz = (b == 1 ? sx.getUint8(ix) : sx.getUint32(ix)), r = []; ix += b;
99 | for (var i = 0; i < sz; i++) r.push(din());
100 | return r;
101 | }
102 | function rut(b) {
103 | var sz = (b == 1 ? sx.getUint8(ix) : sx.getUint32(ix)), r = []; ix += b;
104 | for (var i = 0; i < sz; i++) r.push(din()); din();
105 | return r;
106 | }
107 | function dic(b) {
108 | var sz = sx.getUint32(ix), r = []; ix += 4;
109 | for (var i = 0; i < sz; i++) r.push({k:din(),v:din()});
110 | return r;
111 | }
112 | function iee(x) {
113 | return read_Float(new Uint8Array(sx.buffer.slice(ix,ix+=8)),0,false,52,8);
114 | }
115 | function flo(x) {
116 | return parseFloat(utf8_arr(sx.buffer.slice(ix, ix += 31)));
117 | }
118 | function arr(b) {
119 | var dv, sz = sx.getUint16(ix); ix += b;
120 | return new Uint8Array(sx.buffer.slice(ix, ix += sz));
121 | }
122 | function ref(cr) {
123 | var adj = sx.getUint8(ix++), d; adj += sx.getUint8(ix++);
124 | d = din(); ix += cr+adj*4; return d;
125 | }
126 |
127 | function din() {
128 | var c = sx.getUint8(ix++), x;
129 | switch (c) {
130 | case 97: x = [int, 1]; break; case 98: x = [int, 4]; break;
131 | case 99: x = [flo, 0]; break; case 70: x = [iee, 0]; break;
132 | case 100: x = [str, 2]; break; case 104: x = [run, 1]; break;
133 | case 107: x = [arr, 2]; break; case 108: x = [rut, 4]; break;
134 | case 109: x = [str, 4]; break; case 110: x = [big, 1]; break;
135 | case 111: x = [big, 4]; break; case 115: x = [str, 1]; break;
136 | case 118: x = [str, 2]; break; case 119: x = [str, 1]; break;
137 | case 105: x = [run, 4]; break; case 116: x = [dic, 4]; break;
138 | case 90: x = [ref, 4]; break; case 114: x = [ref, 1]; break;
139 | default: x = [nop, 0];
140 | } return { t: c, v: x[0](x[1]) };
141 | }
142 |
143 | // HELPERS
144 |
145 | function int_to_bytes(Int) {
146 | if (Int % 1 !== 0) return [0];
147 | var isNegative, OriginalInt, i, Rem, s = [];
148 | isNegative = (Int < 0);
149 | if (isNegative) { Int = - Int - 1; }
150 | OriginalInt = Int;
151 | var length = 0;
152 | while (Int !== 0) { Rem = Int % 256;
153 | if (isNegative) { Rem = 255 - Rem; }
154 | s.push(Rem); Int = Math.floor(Int / 256); length++; }
155 | if (Int > 0) { throw ("Argument out of range: " + OriginalInt); }
156 | return s;
157 | }
158 | function uc(u1, u2) {
159 | if (u1.byteLength == 0) return u2; if (u2.byteLength == 0) return u1;
160 | var a = new Uint8Array(u1.byteLength + u2.byteLength);
161 | a.set(u1, 0); a.set(u2, u1.byteLength); return a;
162 | }
163 | function ar(o) {
164 | return o.v instanceof ArrayBuffer ? new Uint8Array(o.v) : o.v instanceof Uint8Array ? o.v :
165 | Array.isArray(o.v) ? new Uint8Array(o.v) : new Uint8Array(utf8_enc(o.v));
166 | }
167 | function fl(a) {
168 | return a.reduce(function (f, t) {
169 | return uc(f, t instanceof Uint8Array ? t :
170 | Array.isArray(t) ? fl(t) : new Uint8Array([t]));
171 | }, new Uint8Array());
172 | }
173 |
--------------------------------------------------------------------------------
/priv/ftp.js:
--------------------------------------------------------------------------------
1 |
2 | // N2O File Transfer Protocol
3 |
4 | function uuid() {
5 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
6 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); });
7 | }
8 |
9 | var ftp = {
10 | queue: [],
11 | init: function (file) {
12 | var item = {
13 | id: uuid(),
14 | status: 'init',
15 | autostart: ftp.autostart || false,
16 | name: ftp.filename || file.name,
17 | sid: ftp.sid || token(), // co(session),
18 | meta: ftp.meta || bin(client()),
19 | offset: ftp.offset || 0,
20 | block: 1,
21 | total: file.size,
22 | file: file,
23 | active: false
24 | };
25 | ftp.queue.push(item);
26 | ftp.send(item, '', 1);
27 | return item.id;
28 | },
29 | start: function (id) {
30 | var item = id ? ftp.item(id) : ftp.next();
31 | if (item) {
32 | if (item.active) { id && (item.autostart = true); return false; }
33 | else {item.active = true; ftp.send_slice(item);}
34 | }
35 | },
36 | stop: function (id) {
37 | var item = id ? ftp.item(id) : ftp.next();
38 | if (item) item.active = false;
39 | },
40 | abort: function(id) {
41 | var item = ftp.item(id);
42 | var index = ftp.queue.indexOf(item);
43 | ftp.queue.splice(index, 1);
44 | if (item) item.active = false;
45 | },
46 | send: function (item, data) {
47 | ws.send(enc(tuple(atom('ftp'),
48 | bin(item.id),
49 | bin(item.sid),
50 | bin(item.name),
51 | item.meta,
52 | number(item.total),
53 | number(item.offset),
54 | number(item.block || data.byteLength),
55 | bin(data),
56 | bin(item.status || 'send'),
57 | list()
58 | )));
59 | },
60 | send_slice: function (item) {
61 | this.reader = new FileReader();
62 | this.reader.onloadend = function (e) {
63 | var res = e.target, data = e.target.result;
64 | if (res.readyState === FileReader.DONE && data.byteLength >= 0) {
65 | ftp.send(item, data);
66 | }
67 | };
68 | this.reader.readAsArrayBuffer(item.file.slice(item.offset, item.offset + item.block));
69 | },
70 | item: function (id) { return ftp.queue.find(function (item) { return item && item.id === id; }); },
71 | next: function () { return ftp.queue.find(function (next) { return next && next.autostart }); }
72 | };
73 |
74 | $file.progress = function onprogress(offset,total) {
75 | var x = qi('ftp_status'); if (x) x.innerHTML = offset;
76 | };
77 |
78 | $file.do = function (rsp) {
79 | var total = rsp.v[5].v, offset = rsp.v[6].v, block = rsp.v[7].v, status = utf8_arr(rsp.v[9].v);
80 | switch (status) {
81 | case 'init':
82 | if(block == 1) return;
83 | var item = ftp.item(utf8_arr(rsp.v[1].v)) || '0';
84 | item.offset = offset;
85 | item.block = block;
86 | item.name = utf8_arr(rsp.v[3].v);
87 | item.status = undefined;
88 | if (item.autostart) ftp.start(item.id);
89 | break;
90 | case 'send':
91 | $file.progress(offset,total);
92 | var item = ftp.item(utf8_arr(rsp.v[1].v));
93 | if (item) item.offset = offset;
94 | if (item) item.block = block;
95 | (block > 0 && (item && item.active)) ? ftp.send_slice(item) : ftp.stop(item.id)
96 | break;
97 | case 'relay': debugger; if (typeof ftp.relay === 'function') ftp.relay(rsp); break;
98 | }
99 | };
100 |
--------------------------------------------------------------------------------
/priv/heart.js:
--------------------------------------------------------------------------------
1 |
2 | // WebSocket Transport
3 |
4 | $ws = { heart: true, interval: 5000,
5 | creator: function(url) { return window.WebSocket ? new window.WebSocket(url) : false; },
6 | onheartbeat: function() { this.channel.send('PING');
7 | } };
8 |
9 | // N2O Reliable Connection
10 |
11 | $conn = { onopen: nop, onmessage: nop, onclose: nop, onconnect: nop,
12 | send: function(data) { if (this.port.channel) this.port.channel.send(data); },
13 | close: function() { if (this.port.channel) this.port.channel.close(); } };
14 |
15 | ct = 0;
16 | transports = [ $ws ];
17 | heartbeat = null;
18 | reconnectDelay = 1000;
19 | maxReconnects = 100;
20 |
21 | function nop() { }
22 | function bullet(url) { $conn.url = url; return $conn; }
23 | function xport() { return maxReconnects <= ct ? false : transports[ct++ % transports.length]; }
24 | function reconnect() { setTimeout(function() { connect(); }, reconnectDelay); }
25 | function next() { $conn.port = xport(); return $conn.port ? connect() : false; }
26 | function connect() {
27 | $conn.port.channel = $conn.port.creator($conn.url);
28 | if (!$conn.port.channel) return next();
29 | $conn.port.channel.binaryType = "arraybuffer";
30 | $conn.port.channel.onmessage = function(e) { $conn.onmessage(e); };
31 | $conn.port.channel.onopen = function() {
32 | if ($conn.port.heart) heartbeat = setInterval(function(){$conn.port.onheartbeat();}, $conn.port.interval);
33 | $conn.onopen();
34 | $conn.onconnect(); };
35 | $conn.port.channel.onclose = function() { $conn.onclose(); clearInterval(heartbeat); reconnect(); };
36 | return $conn; }
37 |
--------------------------------------------------------------------------------
/priv/ieee754.js:
--------------------------------------------------------------------------------
1 |
2 | function read_Float(buffer, offset, isLE, mLen, nBytes) {
3 | var e, m
4 | var eLen = (nBytes * 8) - mLen - 1
5 | var eMax = (1 << eLen) - 1
6 | var eBias = eMax >> 1
7 | var nBits = -7
8 | var i = isLE ? (nBytes - 1) : 0
9 | var d = isLE ? -1 : 1
10 | var s = buffer[offset + i]
11 | i += d
12 | e = s & ((1 << (-nBits)) - 1)
13 | s >>= (-nBits)
14 | nBits += eLen
15 | for (; nBits > 0; e = (e * 256) + buffer[offset + i], i += d, nBits -= 8) {}
16 | m = e & ((1 << (-nBits)) - 1)
17 | e >>= (-nBits)
18 | nBits += mLen
19 | for (; nBits > 0; m = (m * 256) + buffer[offset + i], i += d, nBits -= 8) {}
20 | if (e === 0) {
21 | e = 1 - eBias
22 | } else if (e === eMax) {
23 | return m ? NaN : ((s ? -1 : 1) * Infinity)
24 | } else {
25 | m = m + Math.pow(2, mLen)
26 | e = e - eBias
27 | }
28 | return (s ? -1 : 1) * m * Math.pow(2, e - mLen)
29 | }
30 |
31 | function write_Float(buffer, value, offset, isLE, mLen, nBytes) {
32 | var e, m, c
33 | var eLen = (nBytes * 8) - mLen - 1
34 | var eMax = (1 << eLen) - 1
35 | var eBias = eMax >> 1
36 | var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0)
37 | var i = isLE ? 0 : (nBytes - 1)
38 | var d = isLE ? 1 : -1
39 | var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0
40 | value = Math.abs(value)
41 | if (isNaN(value) || value === Infinity) {
42 | m = isNaN(value) ? 1 : 0
43 | e = eMax
44 | } else {
45 | e = Math.floor(Math.log(value) / Math.LN2)
46 | if (value * (c = Math.pow(2, -e)) < 1) {
47 | e--
48 | c *= 2
49 | }
50 | if (e + eBias >= 1) {
51 | value += rt / c
52 | } else {
53 | value += rt * Math.pow(2, 1 - eBias)
54 | }
55 | if (value * c >= 2) {
56 | e++
57 | c /= 2
58 | }
59 | if (e + eBias >= eMax) {
60 | m = 0
61 | e = eMax
62 | } else if (e + eBias >= 1) {
63 | m = ((value * c) - 1) * Math.pow(2, mLen)
64 | e = e + eBias
65 | } else {
66 | m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen)
67 | e = 0
68 | }
69 | }
70 | for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {}
71 | e = (e << mLen) | m
72 | eLen += mLen
73 | for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {}
74 | buffer[offset + i - d] |= s * 128
75 | }
76 |
--------------------------------------------------------------------------------
/priv/mq.js:
--------------------------------------------------------------------------------
1 | var match, pl = /\+/g, search = /([^&=]+)=?([^&]*)/g,
2 | decode_uri = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
3 | query = window.location.search.substring(1),
4 | nodes = 4,
5 | params = {}; while (match = search.exec(query)) params[decode_uri(match[1])] = decode_uri(match[2]);
6 | var l = location.pathname,
7 | x = l.substring(l.lastIndexOf("/") + 1),
8 | ll = x.lastIndexOf("."),
9 | module = x == "" ? "index" : (ll > 0 ? x.substring(0, ll) : x);
10 |
11 | function gen_client() { return Math.random().toString(36).substring(2) + (new Date()).getTime().toString(36); }
12 | function client() { var c = localStorage.getItem("client"), a;
13 | if (null == c) { c = 'emqttd_' + gen_client(); }
14 | localStorage.setItem("client", c); return c; }
15 | function owner() { return '34239034'; }
16 |
17 | var mqtt = mqtt || {};
18 |
19 | (function(cl,page) {
20 | function token() { return localStorage.getItem("token") || ''; };
21 | function actions(pre) {
22 | let cl = '#' || client();
23 | return pre + "/" + page + "/" + cl;}
24 | function events(pre,srv) {
25 | let o = owner(),
26 | cl = "" || "/" + client(),
27 | version = "1",
28 | s = srv || 'none',
29 | t = token(),
30 | tk = t ? "/"+t :'';
31 | return pre + "/" + o + "/"+ s + "/" + page + "/" + version +"/" + rnd() + cl; }
32 |
33 | function rnd() { return Math.floor((Math.random() * nodes)+1); }
34 | function base() { let d = {host: host, ws_port: 8083 },
35 | b = sessionStorage.base || JSON.stringify(d);
36 | try{return JSON.parse(b);}catch(e){return d;} }
37 | let c = null,
38 | b = {},
39 | opt = {
40 | timeout: 2,
41 | userName: page,
42 | password: token(),
43 | cleanSession: false,
44 | onFailure: failc,
45 | onSuccess: function() {
46 | console.log("MQTT Connected: ", b.host + ':' + b.ws_port);
47 | subscribe(actions("/actions")); }
48 | },
49 | sopt = {
50 | timeout: 2,
51 | qos: 2,
52 | invocationContext: { foo: true },
53 | onFailure: fails,
54 | onSuccess: init
55 | };
56 |
57 | function fails(m){ console.log("MQTT Subscription error:", m.errorMessage);}
58 | function failc(m){ console.log("MQTT Disconnected: ", m.errorMessage); }
59 | function init(x) { send(enc(tuple(atom('init'), bin(token()))), page); }
60 |
61 | function connect() { return c && c.connect(opt); }
62 | function disconnect() { return c && c.disconnect(); }
63 | function subscribe(topic) {
64 | console.log("MQTT Subscribe:", topic);
65 | return c && c.subscribe(topic, sopt);
66 | }
67 | function send(payload, service, qos) {
68 | let s = service || page,
69 | et = events("/events", s);
70 | c && c.send(et, payload, qos || 2, false);
71 | }
72 | function receive(m) {
73 | var BERT = m.payloadBytes.buffer.slice(m.payloadBytes.byteOffset,
74 | m.payloadBytes.byteOffset + m.payloadBytes.length);
75 |
76 | try {
77 | var erlang = dec(BERT);
78 | for (var i = 0; i < $bert.protos.length; i++) {
79 | p = $bert.protos[i];
80 | if (p.on(erlang, p.do).status == "ok") return;
81 | }
82 | } catch (e) { console.log(e); }
83 | }
84 |
85 | function boot() {
86 | try { disconnect();} catch(e){};
87 | b = base();
88 | c = new Paho.MQTT.Client(b.host, b.ws_port, client());
89 | c.onConnectionLost = failc;
90 | c.onMessageArrived = receive;
91 | connect();
92 | }
93 |
94 | boot();
95 |
96 | mqtt.connect = connect;
97 | mqtt.disconnect = disconnect;
98 | mqtt.send = send;
99 | mqtt.subscribe = subscribe;
100 | mqtt.reboot = boot
101 | })(mqtt, module);
102 |
103 | var ws = ws || mqtt;
104 |
--------------------------------------------------------------------------------
/priv/n2o.js:
--------------------------------------------------------------------------------
1 |
2 | // N2O CORE
3 |
4 | var active = false,
5 | debug = false,
6 | persistent = false,
7 | tokenStorage = persistent ? window.localStorage : window.sessionStorage,
8 | protocol = window.location.protocol == 'https:' ? "wss://" : "ws://",
9 | querystring = window.location.pathname + window.location.search,
10 | host = window.location.hostname;
11 |
12 | function client() { return ''; }
13 | function token() { return tokenStorage.getItem("token") || ''; };
14 | function qi(name) { return document.getElementById(name); }
15 | function qs(name) { return document.querySelector(name); }
16 | function qa(name) { return document.querySelectorAll(name); }
17 | function qn(name) { return document.createElement(name); }
18 | function is(x, num, name) { return x == undefined ? false : (x.t == 106 ? false : (x.v.length === num && x.v[0].v === name)); }
19 | function co(name) { match = document.cookie.match(new RegExp(name + '=([^;]+)')); return match ? match[1] : undefined; }
20 |
21 | function N2O_start() {
22 | document.cookie = 'X-Auth-Token=' + token() + '; SameSite=Lax; path=/';
23 | ws = new bullet(protocol + host + (port==""?"":":"+port) + "/ws" + querystring);
24 | ws.onmessage = function (evt) { // formatters loop
25 | for (var i=0;i 0)) {
57 | try {
58 | var erlang = dec(evt.data);
59 | if (typeof cb == 'function') cb(erlang);
60 | for (var i = 0; i < $bert.protos.length; i++) {
61 | var p = $bert.protos[i];
62 | var ret = p.on(erlang, p.do);
63 | if (ret != undefined && ret.status == "ok") return ret;
64 | }
65 | } catch (e) { console.error(e); }
66 | return { status: "ok" };
67 | } else return { status: "error", desc: "data" };
68 | }
69 |
70 | var protos = [$bert];
71 |
--------------------------------------------------------------------------------
/priv/utf8.js:
--------------------------------------------------------------------------------
1 |
2 | // N2O UTF-8 Support
3 |
4 | function utf8_dec(ab) { return (new TextDecoder()).decode(ab); }
5 | function utf8_enc(ab) { return (new TextEncoder("utf-8")).encode(ab); }
6 | function utf8_arr(ab) { if (!(ab instanceof ArrayBuffer)) ab = new Uint8Array(utf8_enc(ab)).buffer;
7 | return utf8_dec(ab); }
8 |
--------------------------------------------------------------------------------
/priv/xhr.js:
--------------------------------------------------------------------------------
1 |
2 | // N2O XHR Fallback
3 |
4 | // WebSocket = undefined; // to test
5 |
6 | $xhr = { heart: false, interval: 100, creator: function(url) { $conn.url = xhr_url(url);
7 | $xhr.channel = { send: xhr_send, close: xhr_close }; $conn.onopen();
8 | return $xhr.channel; }, onheartbeat: function() { xhr('POST',{});} };
9 |
10 | transports = [$ws,$xhr];
11 |
12 | function xhr_header(request) { request.setRequestHeader('Content-Type','application/x-www-form-urlencoded; charset=utf-8'); }
13 | function xhr_url(url) { return url.replace('ws:', 'http:').replace('wss:', 'https:'); }
14 | function xhr_close() { $conn.onclose(); clearInterval(heartbeat); }
15 | function xhr_send(data) { return xhr('POST',data); }
16 | function xhr_receive(data) { if (data.length != 0) $conn.onmessage({'data':data}); }
17 | function xhr(method,data) {
18 | var request = new XMLHttpRequest();
19 | request.open(method,$conn.url,true);
20 | xhr_header(request);
21 | request.onload = function() { console.log(request.response); xhr_receive(request.response); };
22 | request.send(data);
23 | return true; }
24 |
--------------------------------------------------------------------------------
/priv/zlib.js:
--------------------------------------------------------------------------------
1 | function ZLib_inflate(source, options = {}) {
2 | let result = null;
3 | [firstByte, source] = source[0] == 131 && source[1] == 80 ? [source[0], source.slice(6)] : [null, source];
4 | let inflate = new Inflate(source, _decodeZlibHeader(source, 0).length, options);
5 | let buffer = inflate.decompress();
6 | inflate = null;
7 | if(firstByte) {
8 | result = new Uint8Array(buffer.length + 1);
9 | result.set([firstByte]);
10 | result.set(buffer, 1);
11 | } else {
12 | result = buffer;
13 | }
14 | return result;
15 | }
16 |
17 | function _decodeZlibHeader(source, cursor) {
18 | var ZLIB_COMPRESSION_METHOD_DEFLATE = 0x8;
19 | var cmf = source[cursor];
20 | var flg = source[cursor + 1];
21 | var checkSum = ((cmf << 8) + flg) % 31;
22 | var CINFO = (cmf >> 4) & 0x0f, CM = (cmf) & 0x0f;
23 | var FLEVEL = (flg >> 6) & 0x03, FDICT = (flg >> 5) & 0x01, FCHECK = (flg) & 0x1f;
24 | var zlibHeader = {CMF: {CINFO, CM}, FLG: {FLEVEL, FDICT, FCHECK}, length: 2};
25 | let error = checkSum !== 0 ? new TypeError("zlib header check-sum error") :
26 | zlibHeader.CMF.CM !== ZLIB_COMPRESSION_METHOD_DEFLATE ? new TypeError("zlib header unsupported compression method") :
27 | zlibHeader.FLG.FDICT ? new TypeError("zlib header FDICT is not supported") : null;
28 | if(error) throw error;
29 | return zlibHeader;
30 | }
31 |
32 | function Huffman(source) {
33 | var maxCodeLength = Math.max.apply(0, source);
34 | var minCodeLength = Math.min.apply(0, source);
35 | var skipLength = 2;
36 | var bitLength = 1;
37 | var huffmanCode = 0;
38 | var huffmanCodeTableSize = 1 << maxCodeLength;
39 | var huffmanCodeTable = new Uint32Array(huffmanCodeTableSize);
40 |
41 | while (bitLength <= maxCodeLength) {
42 | source.forEach((len, i) => {
43 | if (len === bitLength) {
44 | var code = huffmanCode;
45 | var j = 0, k = 0;
46 | for (; j < bitLength; ++j) { k = (k << 1) | (code & 1); code >>= 1; }
47 | var value = (bitLength << 16) | i;
48 | for (; k < huffmanCodeTableSize; k += skipLength) { huffmanCodeTable[k] = value; }
49 | ++huffmanCode;
50 | }
51 | });
52 | ++bitLength;
53 | huffmanCode <<= 1;
54 | skipLength <<= 1;
55 | }
56 | return {huffmanCodeTable, maxCodeLength, minCodeLength};
57 | }
58 |
59 | function Inflate(input, cursor, options = {}) {
60 | this._streamBuffer = input;
61 | this._streamCursor = cursor;
62 | this._outputBuffer = new Uint8Array(options["bufferSize"] || 0x8000);
63 | this._outputCursor = 0;
64 | this._lastRLE = 0;
65 | this._litlenTable = null;
66 | this._bitStreamReaderBuffer = 0;
67 | this._bitStreamReaderBufferSize = 0;
68 | }
69 |
70 | Inflate.prototype.decompress = function Inflate_decompress() {
71 | while (_parseDeflatedBlock(this)) {}
72 | var result = this._outputBuffer.subarray(0, this._outputCursor);
73 | this._streamBuffer = null;
74 | this._outputBuffer = null;
75 | return result;
76 | }
77 |
78 | let _parseDeflatedBlock = that => {
79 | var bfinal = _readBits(that, 1);
80 | _readBits(that, 2) == BTYPE_FIXED_HUFFMAN ? _parseFixedHuffmanBlock(that) : _parseDynamicHuffmanBlock(that);
81 | return !bfinal;
82 | }
83 |
84 | let _expandOutputBuffer = that => {
85 | let newOutputBuffer = new Uint8Array(that._outputBuffer.length * 2);
86 | newOutputBuffer.set(that._outputBuffer);
87 | that._outputBuffer = newOutputBuffer;
88 | return that._outputBuffer;
89 | }
90 |
91 | let valuesConstructor = ({_bitStreamReaderBuffer, _bitStreamReaderBufferSize, _streamBuffer}) => {
92 | return [_bitStreamReaderBuffer, _bitStreamReaderBufferSize, _streamBuffer, _streamBuffer.length];
93 | }
94 |
95 | function _readBits(that, entity) {
96 | [bitsbuf, bitsbuflen, streamBuffer, streamLength] = valuesConstructor(that);
97 | let bitLength = entity instanceof Object ? entity.maxCodeLength : entity;
98 | let readerBuffer = null;
99 | let readerBufferSize = null;
100 | let result = null;
101 | while (bitsbuflen < bitLength) {
102 | if(that._streamCursor >= streamLength) break;
103 | bitsbuf |= streamBuffer[that._streamCursor++] << bitsbuflen;
104 | bitsbuflen += 8;
105 | }
106 | if(entity instanceof Object) {
107 | readerBuffer = bitsbuf >> (entity.huffmanCodeTable[bitsbuf & ((1 << bitLength) - 1)] >>> 16);
108 | readerBufferSize = bitsbuflen - (entity.huffmanCodeTable[bitsbuf & ((1 << bitLength) - 1)] >>> 16);
109 | result = entity.huffmanCodeTable[bitsbuf & ((1 << bitLength) - 1)] & 0xffff;
110 | } else {
111 | result = bitsbuf & ((1 << bitLength) - 1);
112 | bitsbuf >>>= bitLength;
113 | bitsbuflen -= bitLength
114 | readerBuffer = bitsbuf;
115 | readerBufferSize = bitsbuflen;
116 | }
117 | that._bitStreamReaderBuffer = readerBuffer;
118 | that._bitStreamReaderBufferSize = readerBufferSize;
119 | return result;
120 | }
121 |
122 | let _parseFixedHuffmanBlock = that => {
123 | that._litlenTable = FIXED_HUFFMAN_LENGTH_CODE_TABLE;
124 | _decodeHuffmanAdaptive(that, FIXED_HUFFMAN_DISTANCE_CODE_TABLE);
125 | }
126 |
127 | function _decodeHuffmanAdaptive(that, dist) {
128 | var outputBuffer = that._outputBuffer;
129 | var outputCursor = that._outputCursor;
130 | var outputBufferLength = outputBuffer.length;
131 | var huffmanCode = 0;
132 | while ((huffmanCode = _readBits(that, that._litlenTable)) !== 256) {
133 | if (huffmanCode < 256) {
134 | outputBuffer = outputCursor >= outputBufferLength ? _expandOutputBuffer(that) : outputBuffer;
135 | outputBufferLength = outputBuffer.length;
136 | outputBuffer[outputCursor++] = huffmanCode;
137 | } else {
138 | var tableCursor = huffmanCode - 257;
139 | var huffmanCodeLength = HUFFMAN_LENGTH_CODE_TABLE[tableCursor];
140 | var huffmanBitsLength = HUFFMAN_LENGTH_EXTRA_BITS_TABLE[tableCursor];
141 | huffmanCodeLength += huffmanBitsLength > 0 ? _readBits(that, huffmanBitsLength) : 0;
142 | huffmanCode = _readBits(that, dist);
143 | var huffmanCodeDist = HUFFMAN_DIST_CODE_TABLE[huffmanCode];
144 | var huffmanBitsDist = HUFFMAN_DIST_EXTRA_BITS_TABLE[huffmanCode];
145 | huffmanCodeDist += huffmanBitsDist > 0 ? _readBits(that, huffmanBitsDist) : 0;
146 | outputBuffer = outputCursor + huffmanCodeLength > outputBufferLength ? _expandOutputBuffer(that) : outputBuffer;
147 | outputBufferLength = outputBuffer.length;
148 | while (huffmanCodeLength--) { outputBuffer[outputCursor] = outputBuffer[(outputCursor++) - huffmanCodeDist]; }
149 | }
150 | }
151 | while (that._bitStreamReaderBufferSize >= 8) {
152 | that._bitStreamReaderBufferSize -= 8;
153 | that._streamCursor--;
154 | }
155 | that._outputCursor = outputCursor;
156 | }
157 |
158 | function _parseDynamicHuffmanBlock(that) {
159 | var hlit = _readBits(that, 5) + 257;
160 | var hdist = _readBits(that, 5) + 1;
161 | var hclen = _readBits(that, 4) + 4;
162 | var codeLengths = new Uint8Array(HUFFMAN_ORDER.length);
163 | for (var i = 0; i < hclen; ++i) { codeLengths[HUFFMAN_ORDER[i]] = _readBits(that, 3); }
164 | var codeLengthsTable = Huffman(codeLengths);
165 | var literalAndLengthCode = new Uint8Array(hlit);
166 | var distanceCodeLengths = new Uint8Array(hdist);
167 | that._lastRLE = 0;
168 | that._litlenTable = Huffman(_decodeDynamicHuffman(that, hlit, codeLengthsTable, literalAndLengthCode));
169 |
170 | _decodeHuffmanAdaptive(that, Huffman(_decodeDynamicHuffman(that, hdist, codeLengthsTable, distanceCodeLengths)));
171 | }
172 |
173 | function _decodeDynamicHuffman(that, loop, table, lengths) {
174 | var rle = that._lastRLE;
175 | for (var i = 0; i < loop; ) {
176 | var code = _readBits(that, table);
177 | var repeat = 0;
178 | switch (code) {
179 | case 16:
180 | repeat = 3 + _readBits(that, 2);
181 | while (repeat--) { lengths[i++] = rle; }
182 | break;
183 | case 17:
184 | repeat = 3 + _readBits(that, 3);
185 | while (repeat--) { lengths[i++] = 0; }
186 | rle = 0;
187 | break;
188 | case 18:
189 | repeat = 11 + _readBits(that, 7);
190 | while (repeat--) { lengths[i++] = 0; }
191 | rle = 0;
192 | break;
193 | default:
194 | lengths[i++] = code;
195 | rle = code;
196 | }
197 | }
198 | that._lastRLE = rle;
199 | return lengths;
200 | }
201 |
202 | var HUFFMAN_LENGTH_CODE_TABLE = new Uint16Array([
203 | 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000a, 0x000b,
204 | 0x000d, 0x000f, 0x0011, 0x0013, 0x0017, 0x001b, 0x001f, 0x0023, 0x002b,
205 | 0x0033, 0x003b, 0x0043, 0x0053, 0x0063, 0x0073, 0x0083, 0x00a3, 0x00c3,
206 | 0x00e3, 0x0102, 0x0102, 0x0102
207 | ]);
208 | var HUFFMAN_DIST_CODE_TABLE = new Uint16Array([
209 | 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0007, 0x0009, 0x000d, 0x0011,
210 | 0x0019, 0x0021, 0x0031, 0x0041, 0x0061, 0x0081, 0x00c1, 0x0101, 0x0181,
211 | 0x0201, 0x0301, 0x0401, 0x0601, 0x0801, 0x0c01, 0x1001, 0x1801, 0x2001,
212 | 0x3001, 0x4001, 0x6001
213 | ]);
214 | var HUFFMAN_ORDER = new Uint16Array([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
215 | var HUFFMAN_LENGTH_EXTRA_BITS_TABLE = new Uint8Array([
216 | 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0
217 | ]);
218 | var HUFFMAN_DIST_EXTRA_BITS_TABLE = new Uint8Array([
219 | 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13
220 | ]);
221 | var FIXED_HUFFMAN_LENGTH_CODE_TABLE = (() => {
222 | let lengths = new Uint8Array(288).map((el, i) => (i <= 143) ? 8 : (i <= 255) ? 9 : (i <= 279) ? 7 : 8);
223 | return Huffman(lengths);
224 | })();
225 | var FIXED_HUFFMAN_DISTANCE_CODE_TABLE = (() => new Huffman(new Uint8Array(30).fill(5)))();
226 | var BTYPE_UNCOMPRESSED = 0;
227 | var BTYPE_FIXED_HUFFMAN = 1;
228 | var BTYPE_DYNAMIC_HUFFMAN = 2;
229 | var BTYPE_UNKNOWN = 3;
230 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | {erl_opts, [nowarn_export_all,nowarn_deprecated_function]}.
2 | {deps,[]}.
3 | {project_plugins, [rebar3_format]}.
4 | {format, [
5 | {files, ["src/*.erl", "test/*.erl"]},
6 | {formatter, otp_formatter},
7 | {options, #{ line_length => 108,
8 | paper => 250,
9 | spaces_around_fields => false,
10 | inlining => all,
11 | inline_clause_bodies => true,
12 | inline_expressions => true,
13 | inline_qualified_function_composition => true,
14 | inline_simple_funs => true,
15 | inline_items => all,
16 | inline_fields => true,
17 | inline_attributes => true
18 | }}]}.
19 |
--------------------------------------------------------------------------------
/src/mqtt/n2o_mqtt.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_mqtt).
2 | -export([proc/2,send/3]).
3 | -include_lib("n2o/include/n2o.hrl").
4 |
5 | send(C,T,M) ->
6 | emqtt:publish(C,T,M).
7 |
8 | proc(init,#pi{name=Name}=Async) ->
9 | process_flag(trap_exit, true),
10 |
11 | [_,Srv,Node|_] = string:tokens(Name, "/"),
12 |
13 | Opt = proplists:get_value(list_to_atom(Srv), application:get_env(n2o, mqtt_services, []), []),
14 | Own = proplists:get_value(owner, Opt, none),
15 | Ps = proplists:get_value(protocols, Opt, []),
16 | QoS = proplists:get_value(qos, Opt, 2),
17 |
18 | Topic = {?EV_TOPIC(Own,Srv,Node),QoS},
19 |
20 | case emqtt:start_link(#{owner => self(),
21 | client_id => Name,
22 | host => application:get_env(n2o, mqtt_host, {127,0,0,1}),
23 | port => application:get_env(n2o, mqtt_tcp_port, 1883)}) of
24 | {ok, Conn} ->
25 | case emqtt:connect(Conn) of
26 | {ok,_} -> case emqtt:subscribe(Conn, Topic) of {ok,_,_} ->
27 | {ok,Async#pi{state=#mqcn{conn=Conn, proto=Ps}}};
28 | {error, Error} -> {error, Error} end;
29 | {error, Error} -> {error, Error} end;
30 | ignore -> ignore;
31 | {error, Error} -> {error, Error}
32 | end;
33 |
34 | proc({'EXIT', _Pid, {shutdown, Reason}}, State) ->
35 | io:format("MQTT disconnected ~p.~n State: ~p~n", [Reason, State]),
36 | % transient pi will be restarted if terminated abnormally
37 | {stop, tcp_closed, State};
38 | proc({disconnected, shutdown, tcp_closed}, State) ->
39 | io:format("MQTT disconnected ~p~n", [State]),
40 | {stop, tcp_closed, State};
41 |
42 | proc({ring, Srv, {publish, #{topic:=T} = Request}}, State) ->
43 | io:format("MQTT Ring message ~p. App:~p~n.", [T, Srv]),
44 |
45 | [Ch,Own,_,P,Node,Vsn|Ci] = string:tokens(binary_to_list(T), "/"),
46 | Cid = case Ci of [] -> ""; [Id|_] -> Id end,
47 | T2 = lists:join("/", ["",Ch,Own,atom_to_list(Srv),P,Node,Vsn,Cid]),
48 |
49 | proc({publish, Request#{topic := iolist_to_binary(T2)}}, State);
50 |
51 | proc({publish, _}, State=#pi{state=#mqcn{proto=[]}}) ->
52 | {stop, not_handled, State};
53 |
54 | proc({publish, #{payload := Request, topic := Topic}}, State=#pi{state=#mqcn{conn=C,proto=Ps}}) ->
55 | [_,_Own,_Srv,P,Node,_Vsn|Ci] = string:tokens(binary_to_list(Topic), "/"),
56 |
57 | From = case Ci of [] -> ?ACT_TOPIC(P); [Cid|_] -> ?ACT_TOPIC(P,Cid) end,
58 |
59 | Ctx=#cx{module=list_to_atom(P),node=Node,params=Ci,client_pid=C,from=From},
60 | put(context,Ctx),
61 |
62 | % handle_info may initiate the proc
63 | % so valid response are {noreply,_,_} variations and {stop,_,_}
64 | Req = try n2o:decode(Request) catch error:badarg -> {error, badarg} end,
65 |
66 | case n2o_proto:try_info(Req,[],Ctx,Ps) of
67 | {reply,{_, <<>>},_,_} -> {noreply, State};
68 | {reply,{bert, Term},_,#cx{from=X}} -> send(C,X,n2o_bert:encode(Term)), {noreply, State};
69 | {reply,{json, Term},_,#cx{from=X}} -> send(C,X,n2o_json:encode(Term)), {noreply,State};
70 | {reply,{text, Term},_,#cx{from=X}} -> send(C,X,Term), {noreply, State};
71 | {reply,{binary, Term},_,#cx{from=X}} -> send(C,X,Term), {noreply,State};
72 | {reply,{default,Term},_,#cx{from=X}} -> send(C,X,n2o:encode(Term)), {noreply,State};
73 | {reply,{Encoder,Term},_,#cx{from=X}} -> send(C,X,Encoder:encode(Term)), {noreply, State};
74 | Reply -> ?LOG_ERROR({"Invalid Return", Reply}), {noreply, State}
75 | end;
76 |
77 | proc(Unknown,Async=#pi{name=Name}) ->
78 | io:format("MQTTv5 [~s]: ~p~n",[Name,Unknown]),
79 | {noreply, Async}.
80 |
--------------------------------------------------------------------------------
/src/n2o.app.src:
--------------------------------------------------------------------------------
1 | {application,n2o,
2 | [{description,"N2O MQTT TCP WebSocket"},
3 | {vsn,"10.3.3"},
4 | {registered,[]},
5 | {applications,[kernel,stdlib]},
6 | {mod, {n2o, []}}]}.
7 |
--------------------------------------------------------------------------------
/src/n2o.erl:
--------------------------------------------------------------------------------
1 | -module(n2o).
2 |
3 | -description('N2O Application Server').
4 |
5 | -behaviour(supervisor).
6 |
7 | -behaviour(application).
8 |
9 | -include_lib("n2o/include/n2o.hrl").
10 |
11 | -include_lib("n2o/include/n2o_core.hrl").
12 |
13 | -include_lib("n2o/include/n2o_api.hrl").
14 |
15 | -export([start/2,
16 | stop/1,
17 | init/1,
18 | proc/2,
19 | version/0,
20 | to_binary/1,
21 | bench/0]).
22 |
23 | % SERVICES
24 |
25 | -export([send/2, reg/1, unreg/1, reg/2]).
26 |
27 | -export([pickle/1, depickle/1]).
28 |
29 | -export([encode/1, decode/1]).
30 |
31 | -export([session/1, session/2, user/1, user/0]).
32 |
33 | -export([start_mqtt/0, start_ws/0, start_tcp/0, sid/0]).
34 |
35 | -export([cache/2,
36 | cache/3,
37 | cache/4,
38 | invalidate_cache/1]).
39 |
40 | % START VNODE HASH RING
41 |
42 | init([]) ->
43 | storage_init(),
44 | mq_init(),
45 | {ok, {{one_for_one, 1000, 10}, []}}.
46 |
47 | stop(_) -> ok.
48 |
49 | bench() -> ok.
50 |
51 | start(_, _) ->
52 | S = supervisor:start_link({local, n2o}, n2o, []),
53 | start_timer(),
54 | Space = 4, % ring partitions
55 | Default = [erp], % applications
56 | Protocols = [mqtt, ws, tcp], % protocols
57 | % Generic loop for applications, its rings and exposed protocols
58 | [begin
59 | lists:flatmap(fun Rng({B, _}) -> Rng(B);
60 | Rng(B) ->
61 | Key = fun (I) -> lists:concat(["/", P, "/", B, "/", I])
62 | end,
63 | X = lists:map(fun (I) -> Key(I) end,
64 | lists:seq(1, Space)),
65 | application:set_env(B,
66 | n2o_ring:tab2ring(P),
67 | n2o_ring:new(Space, X)),
68 | X
69 | end,
70 | application:get_env(n2o, n2o_ring:tab2srv(P), Default))
71 | end
72 | || P <- Protocols],
73 | S.
74 |
75 | start_mqtt() ->
76 | lists:map(fun start_mqtt/1, n2o_ring:members(mqtt)).
77 |
78 | start_ws() ->
79 | lists:map(fun start_ws/1, n2o_ring:members(ws)).
80 |
81 | start_tcp() ->
82 | lists:map(fun start_tcp/1, n2o_ring:members(tcp)).
83 |
84 | start_mqtt(Node) ->
85 | n2o_pi:start(#pi{module = n2o_mqtt, table = mqtt,
86 | sup = n2o, state = [], name = Node}).
87 |
88 | start_ws(Node) ->
89 | n2o_pi:start(#pi{module = n2o_ws, table = ws, sup = n2o,
90 | state = [], name = Node}).
91 |
92 | start_tcp(Node) ->
93 | n2o_pi:start(#pi{module = n2o_tcp, table = tcp,
94 | sup = n2o, state = [], name = Node}).
95 |
96 | start_timer() ->
97 | n2o_pi:start(#pi{module = n2o, table = caching,
98 | sup = n2o, state = [], name = "/timer"}).
99 |
100 | % ETS
101 |
102 | opt() -> [set, named_table, {keypos, 1}, public].
103 |
104 | tables() ->
105 | application:get_env(n2o,
106 | tables,
107 | [cookies, file, caching, ws, mqtt, tcp, async]).
108 |
109 | storage_init() -> [ets:new(X, opt()) || X <- tables()].
110 |
111 | % MQ
112 |
113 | mq() -> application:get_env(n2o, mq, n2o_gproc).
114 |
115 | mq_init() -> (mq()):init().
116 |
117 | send(X, Y) -> (mq()):send(X, Y).
118 |
119 | reg(X) -> (mq()):reg(X).
120 |
121 | unreg(X) -> (mq()):unreg(X).
122 |
123 | reg(X, Y) -> (mq()):reg(X, Y).
124 |
125 | % PICKLE
126 |
127 | pickler() ->
128 | application:get_env(n2o, pickler, n2o_secret).
129 |
130 | pickle(Data) -> (pickler()):pickle(Data).
131 |
132 | depickle(SerializedData) ->
133 | (pickler()):depickle(SerializedData).
134 |
135 | % SESSION
136 |
137 | sid() ->
138 | #cx{session = SID} = get(context),
139 | SID.
140 |
141 | session() ->
142 | application:get_env(n2o, session, n2o_session).
143 |
144 | session(Key) ->
145 | Context = get(context),
146 | case Context of
147 | #cx{session = SID} ->
148 | (session()):get_value(SID, Key, []);
149 | _ -> []
150 | end.
151 |
152 | session(Key, Value) ->
153 | #cx{session = SID} = get(context),
154 | (session()):set_value(SID, Key, Value).
155 |
156 | user() ->
157 | case session(user) of
158 | undefined -> [];
159 | E -> E
160 | end.
161 |
162 | user(User) -> session(user, User).
163 |
164 | % FORMAT
165 |
166 | formatter() ->
167 | application:get_env(n2o, formatter, n2o_bert).
168 |
169 | encode(Term) -> (formatter()):encode(Term).
170 |
171 | decode(Term) -> (formatter()):decode(Term).
172 |
173 | % CACHE
174 |
175 | cache(Tab, Key, Value, Till) ->
176 | ets:insert(Tab, {Key, {Till, Value}}),
177 | Value.
178 |
179 | cache(Tab, Key, undefined) -> ets:delete(Tab, Key);
180 | cache(Tab, Key, Value) ->
181 | ets:insert(Tab,
182 | {Key,
183 | {n2o_session:till(calendar:local_time(),
184 | n2o_session:ttl()),
185 | Value}}),
186 | Value.
187 |
188 | cache(Tab, Key) ->
189 | Res = ets:lookup(Tab, Key),
190 | Val = case Res of
191 | [] -> [];
192 | [Value] -> Value;
193 | Values -> Values
194 | end,
195 | case Val of
196 | [] -> [];
197 | {_, {infinity, X}} -> X;
198 | {_, {Expire, X}} ->
199 | case Expire < calendar:local_time() of
200 | true ->
201 | ets:delete(Tab, Key),
202 | [];
203 | false -> X
204 | end
205 | end.
206 |
207 | % TIMER
208 |
209 | proc(init, #pi{} = Async) ->
210 | Timer = timer_restart(ping()),
211 | {ok, Async#pi{state = Timer}};
212 | proc({timer, ping}, #pi{state = Timer} = Async) ->
213 | erlang:cancel_timer(Timer),
214 | n2o:invalidate_cache(caching),
215 | (n2o_session:storage()):invalidate_sessions(),
216 | {noreply, Async#pi{state = timer_restart(ping())}}.
217 |
218 | invalidate_cache(Table) ->
219 | ets:foldl(fun (X, _) -> n2o:cache(Table, element(1, X))
220 | end,
221 | 0,
222 | Table).
223 |
224 | timer_restart(Diff) ->
225 | {X, Y, Z} = Diff,
226 | erlang:send_after(1000 * (Z + 60 * Y + 60 * 60 * X),
227 | self(),
228 | {timer, ping}).
229 |
230 | ping() -> application:get_env(n2o, timer, {0, 1, 0}).
231 |
232 | %%
233 |
234 | to_binary(A) when is_atom(A) ->
235 | atom_to_binary(A, latin1);
236 | to_binary(B) when is_binary(B) -> B;
237 | to_binary(T) when is_tuple(T) -> term_to_binary(T);
238 | to_binary(I) when is_integer(I) ->
239 | to_binary(integer_to_list(I));
240 | to_binary(F) when is_float(F) ->
241 | float_to_binary(F, [{decimals, 9}, compact]);
242 | to_binary(L) when is_list(L) -> iolist_to_binary(L).
243 |
244 | %
245 |
246 | version() ->
247 | proplists:get_value(vsn,
248 | element(2, application:get_all_key(n2o))).
249 |
250 | % END
251 |
--------------------------------------------------------------------------------
/src/n2o_multipart.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_multipart).
2 | -export([init/2,terminate/3]).
3 |
4 | % curl -F file=@./Image.tiff http://localhost:50111/multipart
5 |
6 | acc_multipart(Req0, Acc) ->
7 | case cowboy_req:read_part(Req0) of
8 | {ok, Headers, Req1} -> {ok, Body, Req} = stream_body(Req1, <<>>), acc_multipart(Req, [{Headers,Body}|Acc]);
9 | {done, Req} -> {ok, Acc, Req}
10 | end.
11 |
12 | stream_body(Req0, Acc) ->
13 | case cowboy_req:read_part_body(Req0) of
14 | {more, Data, Req} -> stream_body(Req, << Acc/binary, Data/binary >>);
15 | {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req}
16 | end.
17 |
18 | init(Req, Opts) ->
19 | {ok, Body, Req2} = acc_multipart(Req, []),
20 | {Headers, Data} = hd(Body),
21 | {file, _, _Filename, _ContentType} = cow_multipart:form_data(Headers),
22 | GUID = erp:guid(),
23 | Size = erlang:integer_to_list(erlang:size(Data)),
24 | file:write_file(GUID, Data, [raw, binary]),
25 | Req3 = cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>, <<"guid">> => GUID, <<"size">> => Size },
26 | io_lib:format("File ~s uploaded with size ~s.~n",[GUID,Size]), Req2),
27 | {ok, Req3, Opts}.
28 |
29 | terminate(_Reason, _Req, _State) ->
30 | ok.
31 |
--------------------------------------------------------------------------------
/src/n2o_pi.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_pi).
2 |
3 | -description('N2O Process Instance'). % gen_server replacement
4 |
5 | -include_lib("n2o/include/n2o.hrl").
6 |
7 | -include_lib("n2o/include/n2o_pi.hrl").
8 |
9 | -behaviour(gen_server).
10 |
11 | -export([start_link/1]).
12 |
13 | -export([init/1,
14 | handle_call/3,
15 | handle_cast/2,
16 | handle_info/2,
17 | terminate/2,
18 | code_change/3]).
19 |
20 | -export([start/1,
21 | stop/2,
22 | send/2,
23 | send/3,
24 | cast/2,
25 | cast/3,
26 | pid/2,
27 | restart/2]).
28 |
29 | -define(CALL_TIMEOUT,
30 | application:get_env(n2o, pi_call_timeout, 5000)).
31 |
32 | start(#pi{table = Tab, name = Name, module = Module,
33 | sup = Sup, timeout = Timeout, restart = Restart} =
34 | Async) ->
35 | ChildSpec = {{Tab, Name},
36 | {?MODULE, start_link, [Async]},
37 | Restart,
38 | Timeout,
39 | worker,
40 | [Module]},
41 | case supervisor:start_child(Sup, ChildSpec) of
42 | {ok, Pid} -> {Pid, Async#pi.name};
43 | {ok, Pid, _} -> {Pid, Async#pi.name};
44 | {error, already_present} ->
45 | case supervisor:restart_child(Sup, {Tab, Name}) of
46 | {ok, Pid} -> {Pid, Async#pi.name};
47 | {ok, Pid, _} -> {Pid, Async#pi.name};
48 | {error, running} -> {pid(Tab, Name), Async#pi.name};
49 | {error, _} = X -> X
50 | end;
51 | {error, Reason} -> {error, Reason}
52 | end.
53 |
54 | stop(Tab, Name) ->
55 | case n2o_pi:pid(Tab, Name) of
56 | Pid when is_pid(Pid) ->
57 | try
58 | #pi{sup = Sup} = Async = send(Pid, {get}),
59 | [supervisor:F(Sup, {Tab, Name})
60 | || F <- [terminate_child, delete_child]],
61 | n2o:cache(Tab, {Tab, Name}, undefined),
62 | Async
63 | catch
64 | _X:_Y:_Z -> error
65 | end;
66 | Data -> {error, {not_pid, Data}}
67 | end.
68 |
69 | send(Pid, Message) when is_pid(Pid) ->
70 | try gen_server:call(Pid, Message, ?CALL_TIMEOUT) catch
71 | exit:{normal, _}:_Z -> {exit, normal};
72 | X:Y:Z -> {error, {X, Y, Z}}
73 | end.
74 |
75 | send(Tab, Name, Message) ->
76 | try gen_server:call(n2o_pi:pid(Tab, Name), Message, ?CALL_TIMEOUT) catch
77 | exit:{normal, _}:_Z -> {exit, normal};
78 | X:Y:Z -> {error, {X, Y, Z}}
79 | end.
80 |
81 | cast(Pid, Message) when is_pid(Pid) ->
82 | gen_server:cast(Pid, Message).
83 |
84 | cast(Tab, Name, Message) ->
85 | gen_server:cast(n2o_pi:pid(Tab, Name), Message).
86 |
87 | pid(Tab, Name) -> n2o:cache(Tab, {Tab, Name}).
88 |
89 | restart(Tab, Name) ->
90 | case stop(Tab, Name) of
91 | #pi{} = Async -> start(Async);
92 | Error -> Error
93 | end.
94 |
95 | handle(Mod, Message, Async) ->
96 | case Mod:proc(Message, Async) of
97 | {ok, S} -> {ok, S};
98 | {ok, S, T} -> {ok, S, T};
99 | {stop, X, Y, S} -> {stop, X, Y, S};
100 | {stop, X, S} -> {stop, X, S};
101 | {stop, S} -> {stop, S};
102 | {reply, X, S, T} -> {reply, X, S, T};
103 | {reply, X, S} -> {reply, X, S};
104 | {noreply, X, S} -> {noreply, X, S};
105 | {noreply, S} -> {noreply, S};
106 | {_, S} -> {noreply, S};
107 | S -> {noreply, S}
108 | end.
109 |
110 | start_link(Parameters) ->
111 | gen_server:start_link(?MODULE, Parameters, []).
112 |
113 | code_change(_, State, _) -> {ok, State}.
114 |
115 | handle_call({get}, _, Async) -> {reply, Async, Async};
116 | handle_call(_, _, #pi{module = undefined}) ->
117 | {noreply, []};
118 | handle_call(Message, _, #pi{module = Mod} = Async) ->
119 | handle(Mod, Message, Async).
120 |
121 | handle_cast(_, #pi{module = undefined}) ->
122 | {noreply, []};
123 | handle_cast(Message, #pi{module = Mod} = Async) ->
124 | handle(Mod, Message, Async).
125 |
126 | handle_info(timeout, #pi{module = undefined}) ->
127 | {noreply, []};
128 | handle_info(timeout, #pi{module = Mod} = Async) ->
129 | handle(Mod, timeout, Async);
130 | handle_info(_, #pi{module = undefined}) ->
131 | {noreply, []};
132 | handle_info(Message, #pi{module = Mod} = Async) ->
133 | handle(Mod, Message, Async);
134 | handle_info(_, _) -> {noreply, []}.
135 |
136 | init(#pi{module = Mod, table = Tab, name = Name} =
137 | Handler) ->
138 | n2o:cache(Tab, {Tab, Name}, self(), infinity),
139 | Mod:proc(init, Handler).
140 |
141 | terminate(Reason, #pi{name = Name, table = Tab, module = Mod} = Pi) ->
142 | case erlang:function_exported(Mod, terminate, 2) of true -> Mod:terminate(Reason, Pi); false -> false end,
143 | n2o:cache(Tab, {Tab, Name}, undefined),
144 | ok.
145 |
--------------------------------------------------------------------------------
/src/n2o_proto.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_proto).
2 |
3 | -description('N2O WebSocket Protocol').
4 |
5 | -include_lib("n2o/include/n2o.hrl").
6 |
7 | -export([init/2,
8 | finish/2,
9 | info/3,
10 | info/4,
11 | stream/3,
12 | push/5,
13 | init/4,
14 | terminate/2,
15 | cx/2]).
16 |
17 | -export([try_info/4, try_info/5]).
18 |
19 | -export([protocols/0]).
20 |
21 | % Common protocol for MQTT, TCP and WebSocket
22 |
23 | protocols() ->
24 | application:get_env(n2o, protocols, [n2o_heart]).
25 |
26 | info(M, R, S) -> push(M, R, S, protocols(), []).
27 |
28 | info(M, R, S, Ps) -> push(M, R, S, Ps, []).
29 |
30 | nop(R, S) -> {reply, {binary, <<>>}, R, S}.
31 |
32 | reply(M, R, S) -> {reply, M, R, S}.
33 |
34 | push(_, R, S, [], _) -> nop(R, S);
35 | push(M, R, S, [H], _) ->
36 | case H:info(M, R, S) of
37 | {reply, M1, R1, S1} -> reply(M1, R1, S1);
38 | {unknown, _, _, _} -> nop(R, S)
39 | end;
40 | push(M, R, S, [H | T], Acc) ->
41 | case H:info(M, R, S) of
42 | {unknown, _, _, _} -> push(M, R, S, T, Acc);
43 | {reply, M1, R1, S1} -> reply(M1, R1, S1);
44 | A -> push(M, R, S, T, [A | Acc])
45 | end.
46 |
47 | cx(Cookies, Req) ->
48 | Token = case lists:keyfind(<<"X-Auth-Token">>,
49 | 1,
50 | Cookies)
51 | of
52 | {_, V} -> V;
53 | false -> <<>>
54 | end,
55 | Sid = case n2o:depickle(Token) of
56 | {{S, _}, _} -> S;
57 | _ -> <<>>
58 | end,
59 | #cx{actions = [], path = [], req = Req, params = [],
60 | session = Sid, token = Token,
61 | handlers =
62 | [{routes, application:get_env(n2o, routes, ?MODULE)}]}.
63 |
64 | % Part of WebSocket Adapter
65 |
66 | finish(State, Cx) -> {ok, State, Cx}.
67 |
68 | init(State, Cx) -> {ok, State, Cx}.
69 |
70 | fold(Fun, Handlers, Ctx) ->
71 | lists:foldl(fun ({_, []}, C) -> C;
72 | ({_, Module}, Ctx1) ->
73 | {ok, _, NewCtx} = Module:Fun([], Ctx1),
74 | NewCtx
75 | end,
76 | Ctx,
77 | Handlers).
78 |
79 | terminate(_, #cx{module = Module}) ->
80 | catch Module:event(terminate).
81 |
82 | init(_Transport, Req, _Opts, _) ->
83 | Cookies = cowboy_req:parse_cookies(Req),
84 | {Module, CxInit} = application:get_env(n2o,
85 | cx,
86 | {n2o_proto, cx}),
87 | Zero = Module:CxInit(Cookies, Req),
88 | put(context, Zero),
89 | Ctx = fold(init, Zero#cx.handlers, Zero),
90 | put(context, Ctx),
91 | Origin = case cowboy_req:header(<<"origin">>,
92 | Req,
93 | <<"*">>)
94 | of
95 | {O, _} -> O;
96 | X -> X
97 | end,
98 | ConfigOrigin = iolist_to_binary(application:get_env(n2o,
99 | origin,
100 | Origin)),
101 | Req1 =
102 | cowboy_req:set_resp_header(<<"Access-Control-Allow-Origin">>,
103 | ConfigOrigin,
104 | Ctx#cx.req),
105 | {ok, Req1, Ctx}.
106 |
107 | stream({text, _} = M, R, S) ->
108 | push(M, R, S, protocols(), []);
109 | stream({binary, <<>>}, R, S) -> nop(R, S);
110 | stream({binary, D}, R, S) ->
111 | push(n2o:decode(D), R, S, protocols(), []);
112 | stream(_, R, S) -> nop(R, S).
113 |
114 | try_info(M, R, S, Ps) -> try_info(?MODULE, M, R, S, Ps).
115 |
116 | -ifdef(OTP_RELEASE).
117 |
118 | try_info(Module, M, R, S, Ps) ->
119 | try Module:info(M, R, S, Ps) catch
120 | Err:Rea:Stack ->
121 | ?LOG_EXCEPTION(Err, Rea, Stack),
122 | {error, {stack, Stack}}
123 | end.
124 |
125 | -else.
126 |
127 | try_info(Module, M, R, S, Ps) ->
128 | try Module:info(M, R, S, Ps) catch
129 | Err:Rea ->
130 | Stack = erlang:get_stacktrace(),
131 | ?LOG_EXCEPTION(Err, Rea, Stack),
132 | {error, {stack, Stack}}
133 | end.
134 |
135 | -endif.
136 |
--------------------------------------------------------------------------------
/src/n2o_ring.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_ring).
2 |
3 | -description('N2O Ring').
4 |
5 | -export([add/3,
6 | contains/3,
7 | lookup/3,
8 | members/1,
9 | new/1,
10 | new/2,
11 | remove/3,
12 | size/1,
13 | send/2,
14 | send/3]).
15 |
16 | -export([ring/2, tab2srv/1, tab2ring/1]).
17 |
18 | -define(HASH, sha).
19 |
20 | tab2ring(ws) -> ws_ring;
21 | tab2ring(mqtt) -> mqtt_ring;
22 | tab2ring(tcp) -> tcp_ring.
23 |
24 | tab2srv(ws) -> ws_services;
25 | tab2srv(mqtt) -> mqtt_services;
26 | tab2srv(tcp) -> tcp_services.
27 |
28 | ring({App, _}, Tab) -> ring(App, Tab);
29 | ring(App, Tab) ->
30 | case application:get_env(App, tab2ring(Tab)) of
31 | {ok, R} -> R;
32 | undefined -> n2o_ring:new(1, [1])
33 | end.
34 |
35 | lookup(Tab, App, Term) ->
36 | lookup_index(Term, ring(App, Tab)).
37 |
38 | contains(Tab, App, Term) ->
39 | contains_index(Term, ring(App, Tab)).
40 |
41 | add(Tab, App, Node) -> add_index(Node, ring(App, Tab)).
42 |
43 | remove(Tab, App, Node) ->
44 | remove_index(Node, ring(App, Tab)).
45 |
46 | size(Tab) -> size_index(Tab).
47 |
48 | send(Tab, Msg) ->
49 | n2o_pi:send(Tab,
50 | lookup_index(Msg, ring(n2o, Tab)),
51 | Msg).
52 |
53 | send(Tab, App, Msg) ->
54 | Name = lookup_index(Msg, ring(App, Tab)),
55 | n2o_pi:send(n2o_pi:pid(Tab, Name), {ring, App, Msg}).
56 |
57 | members(Tab) ->
58 | [Z
59 | || {Z}
60 | <- lists:flatten([lists:map(fun (X) -> {X} end,
61 | members_index(ring(App, Tab)))
62 | || App <- application:get_env(n2o, tab2srv(Tab), [])])].
63 |
64 | add_index(Node, {NumVNodes, InnerRing}) ->
65 | NewInnerRing = build_ring(position_node(NumVNodes,
66 | Node),
67 | InnerRing),
68 | {NumVNodes, NewInnerRing}.
69 |
70 | contains_index(Node, {_NumVNodes, InnerRing}) ->
71 | case gb_trees:lookup(chash(Node), InnerRing) of
72 | none -> false;
73 | {value, _} -> true
74 | end.
75 |
76 | lookup_index(Key, {_NumVNodes, InnerRing}) ->
77 | case gb_trees:is_empty(InnerRing) of
78 | true -> {error, empty_ring};
79 | false ->
80 | HKey = chash(Key),
81 | Iter = gb_trees:iterator_from(HKey, InnerRing),
82 | case gb_trees:next(Iter) of
83 | {_, Node, _} -> Node;
84 | none -> element(2, gb_trees:smallest(InnerRing))
85 | end
86 | end.
87 |
88 | new(NumVNodes, Nodes) ->
89 | Ring =
90 | build_ring(lists:flatten([position_node(NumVNodes, Node)
91 | || Node <- Nodes])),
92 | {NumVNodes, Ring}.
93 |
94 | remove_index(Node, {NumVNodes, InnerRing}) ->
95 | Positions = position_node(NumVNodes, Node),
96 | NewInnerRing = lists:foldl(fun ({Pos, _}, Tree) ->
97 | gb_trees:delete_any(Pos, Tree)
98 | end,
99 | InnerRing,
100 | Positions),
101 | {NumVNodes, NewInnerRing}.
102 |
103 | size_index({_NumVNodes, InnerRing}) ->
104 | gb_trees:size(InnerRing).
105 |
106 | build_ring(Nodes) ->
107 | gb_trees:from_orddict(lists:keysort(1, Nodes)).
108 |
109 | build_ring(Nodes, Ring) ->
110 | lists:foldl(fun ({Pos, Node}, Tree) ->
111 | gb_trees:insert(Pos, Node, Tree)
112 | end,
113 | Ring,
114 | Nodes).
115 |
116 | members_index({_NumVNodes, InnerRing}) ->
117 | lists:usort(gb_trees:values(InnerRing)).
118 |
119 | new(Nodes) -> new(1, Nodes).
120 |
121 | chash(X) -> crypto:hash(?HASH, term_to_binary(X)).
122 |
123 | chash(X, Y) ->
124 | XBin = term_to_binary(X),
125 | YBin = term_to_binary(Y),
126 | crypto:hash(?HASH, <>).
127 |
128 | position_node(Node) -> {chash(Node), Node}.
129 |
130 | position_node(1, Node) -> [position_node(Node)];
131 | position_node(NumVNodes, Node) ->
132 | Replicas = [{chash(Node, Idx), Node}
133 | || Idx <- lists:seq(1, NumVNodes - 1)],
134 | [position_node(Node) | Replicas].
135 |
--------------------------------------------------------------------------------
/src/protos/n2o_ftp.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_ftp).
2 | -description('N2O File Protocol').
3 | -include_lib("n2o/include/n2o.hrl").
4 | -include_lib("kernel/include/file.hrl").
5 | -export([info/3,proc/2,filename/1,root/0]).
6 |
7 | -define(ROOT, filename:join(begin {ok, Cwd} = file:get_cwd(), Cwd end,
8 | application:get_env(n2o,upload,code:priv_dir(n2o)))).
9 | -define(NEXT, 256*1024). % 256K chunks for best 25MB/s speed
10 | -define(STOP, 0).
11 |
12 | root() -> ?ROOT.
13 |
14 | % Callbacks
15 |
16 | filename(#ftp{sid=_Sid,filename=FileName}) -> FileName. %filename:join(lists:concat([Sid]),FileName).
17 |
18 | % File Transfer Protocol
19 |
20 | info(#ftp{status = {event, _}}=FTP, Req, State) ->
21 | {reply, {bert, nitro_n2o:io(FTP, State)}, Req, State};
22 |
23 | info(#ftp{id = Link, status = <<"init">>, block = Block, offset = Offset}=FTP, Req, State) ->
24 | Root=?ROOT,
25 | RelPath=(application:get_env(n2o,filename,n2o_ftp)):filename(FTP),
26 | FilePath = filename:join(Root, RelPath),
27 | ok = filelib:ensure_dir(FilePath),
28 | FileSize = case file:read_file_info(FilePath) of
29 | {ok, Fi} -> Fi#file_info.size;
30 | {error, _} -> 0
31 | end,
32 |
33 | % ?LOG_INFO("FTP INFO INIT: ~p",[ FTP#ftp{data = <<>>, sid = <<>>} ]),
34 |
35 | Block2 = case Block of 0 -> ?STOP; _ -> ?NEXT end,
36 | Offset2 = case FileSize >= Offset of true -> FileSize; false -> 0 end,
37 | FTP2 = FTP#ftp{block = Block2, offset = Offset2, data = <<>>, filename=FilePath},
38 |
39 | try n2o_pi:stop(file, Link) catch _X:_Y:_Z -> ok end,
40 | n2o_pi:start(#pi{module=?MODULE, table=file, sup=n2o, state=FTP2, name=Link}),
41 |
42 | {reply, {bert, FTP2}, Req, State};
43 |
44 | info(#ftp{id = Link, status = <<"send">>}=FTP, Req, State) ->
45 | % ?LOG_INFO("FTP SEND: ~p", [FTP#ftp{data = <<>>, sid = <<>>}]),
46 | Reply = try
47 | n2o_pi:send(file, Link, FTP)
48 | catch E:R ->
49 | ?LOG_ERROR(#{error => E, reason => R, loc => ftpinfo}),
50 | FTP#ftp{data = <<>>,sid = <<>>, block = ?STOP}
51 | end,
52 | {reply, {bert, Reply}, Req, State};
53 |
54 | info(Message, Req, State) -> {unknown, Message, Req, State}.
55 |
56 | % n2o Handlers
57 |
58 | proc(init, #pi{state=#ftp{sid = Token} = FTP}=Async) ->
59 | Sid = case n2o:depickle(Token) of {{S,_},_} -> S; X -> X end,
60 | catch n2o:send(Sid, {direct, FTP}),
61 | {ok, Async};
62 |
63 | proc(#ftp{sid = Token, data = Data, status = <<"send">>, block = Block, meta = Cid} = FTP,
64 | #pi{name = Link, state = #ftp{size = TotalSize, offset = Offset, filename = RelPath}} = Async)
65 | when Offset + Block >= TotalSize ->
66 | % ?LOG_INFO("FTP PROC FINALE: ~p~n", [ Link ]),
67 | case file:write_file(filename:join(?ROOT,RelPath), <>, [append,raw]) of
68 | {error, Reason} ->
69 | % ?LOG_ERROR("WRITE ERROR: ~p~n", [ filename:join(?ROOT, RelPath) ]),
70 | {reply, {error, Reason}, Async};
71 | ok ->
72 | FTP2 = FTP#ftp{data = <<>>, sid = <<>>,offset = TotalSize, block = ?STOP},
73 | FTP3 = FTP2#ftp{status = {event, stop}, filename = RelPath},
74 | spawn(fun() ->
75 | [begin
76 | Owner = n2o:to_binary(proplists:get_value(owner, Opt, none)),
77 | Ev = application:get_env(n2o,events_topic,"/events"),
78 | S1 = atom_to_list(S),
79 | Node = integer_to_list(rand:uniform(4)),
80 | Module = atom_to_list(S), %fix: service name not always match module
81 | Topic = iolist_to_binary([Ev,"/",Owner,"/", S1, "/", Module, "/",?VSN, "/",Node, "/", Cid]),
82 | Msg = {publish, #{payload => term_to_binary(FTP3), topic => Topic}},
83 | n2o_ring:send(mqtt, S, Msg)
84 | end || {S,Opt} <- lists:filter(fun({_,O}) ->
85 | lists:member(n2o_ftp,proplists:get_value(protocols, O, [])) end,
86 | application:get_env(n2o, mqtt_services, []))],
87 |
88 | Sid = case n2o:depickle(Token) of {{S,_},_} -> S; X -> X end,
89 | catch n2o:send(Sid, {direct, FTP3}) end),
90 |
91 | spawn(fun() -> try n2o_pi:stop(file, Link) catch _X:_Y:_Z -> ok end end),
92 | {stop, normal, FTP2, Async#pi{state = FTP2}}
93 | end;
94 |
95 | proc(#ftp{data = Data, block = Block} = FTP,
96 | #pi{state = #ftp{offset = Offset, filename = RelPath}}=Async) ->
97 | FTP2 = FTP#ftp{status = <<"send">>, offset = Offset + Block },
98 | case file:write_file(filename:join(?ROOT, RelPath), <>, [append,raw]) of
99 | {error, Reason} -> {reply, {error, Reason}, Async};
100 | ok -> {reply, FTP2#ftp{data = <<>>},
101 | Async#pi{state = FTP2#ftp{data = <<>>, filename = RelPath}}} end;
102 |
103 | proc(_,Async) -> {reply, #ftpack{}, Async}.
104 |
--------------------------------------------------------------------------------
/src/protos/n2o_heart.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_heart).
2 | -description('N2O Heartbeat Protocol').
3 | -include_lib("n2o/include/n2o.hrl").
4 | -export([info/3]).
5 |
6 | info({text,<<"PING">> = _Ping}, Req, State) ->
7 | {reply, {text, <<"PONG">>}, Req, State};
8 |
9 | info({text,<<>>}, Req, State) ->
10 | {reply, {text, <<>>}, Req, State};
11 |
12 | info(Message, Req, State) ->
13 | {unknown,Message, Req, State}.
14 |
--------------------------------------------------------------------------------
/src/services/n2o_bert.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_bert).
2 | -description('N2O BERT Formatter').
3 | -include("n2o.hrl").
4 | -export([encode/1,decode/1]).
5 |
6 | encode(#ftp{}=FTP) -> term_to_binary(setelement(1,FTP,ftpack));
7 | encode(Term) -> term_to_binary(Term).
8 | decode(Bin) -> binary_to_term(Bin).
9 |
10 |
--------------------------------------------------------------------------------
/src/services/n2o_json.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_json).
2 | -description('N2O JSON Formatter').
3 | -include("n2o.hrl").
4 | -export([encode/1,decode/1]).
5 |
6 | encode({_Io,Eval,Data}) ->
7 | jsone:encode([{t,104},{v,[
8 | [{t,100},{v,io}],
9 | [{t,109},{v,Eval}],
10 | [{t,109},{v,Data}]]}]);
11 |
12 | encode({Atom,Data}) ->
13 | jsone:encode([{t,104},{v,[
14 | [{t,100},{v,Atom}],
15 | [{t,109},{v,Data}]]}]).
16 |
17 | decode(Bin) -> jsone:decode(Bin).
18 |
--------------------------------------------------------------------------------
/src/services/n2o_secret.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_secret).
2 | -description('N2O HMAC AES/CBC-128').
3 | -include("n2o.hrl").
4 | -export([pickle/1,depickle/1,hex/1,unhex/1,sid/1,mac/1]).
5 |
6 | mac(Args) ->
7 | case erlang:list_to_integer(erlang:system_info(otp_release)) of
8 | X when X =< 22 -> erlang:apply(crypto,hmac,Args) ;
9 | _ -> erlang:apply(crypto,mac,[hmac]++Args)
10 | end.
11 |
12 | pickle(Data) ->
13 | Message = term_to_binary(Data),
14 | Padding = size(Message) rem 16,
15 | Bits = (16-Padding)*8, Key = secret(), IV = crypto:strong_rand_bytes(16),
16 | Cipher = case erlang:list_to_integer(erlang:system_info(otp_release)) of
17 | X when X =< 22 -> erlang:apply(crypto,block_encrypt,[aes_cbc128,Key,IV,<>]) ;
18 | _ -> erlang:apply(crypto,crypto_one_time,[aes_128_cbc,Key,IV,<>,[{encrypt,true}]])
19 | end,
20 | Signature = mac([application:get_env(n2o,hmac,sha256),Key,<>]),
21 | hex(<>).
22 |
23 | depickle(PickledData) ->
24 | try Key = secret(),
25 | Decoded = unhex(iolist_to_binary(PickledData)),
26 | <> = Decoded,
27 | Signature = mac([application:get_env(n2o,hmac,sha256),Key,<>]),
28 | _Decipher = case erlang:list_to_integer(erlang:system_info(otp_release)) of
29 | X when X =< 22 -> binary_to_term(erlang:apply(crypto,block_decrypt,[aes_cbc128,Key,IV,Cipher]),[safe]) ;
30 | _ -> binary_to_term(erlang:apply(crypto,crypto_one_time,[aes_128_cbc,Key,IV,Cipher,[{encrypt,false}]]),[safe])
31 | end
32 | catch _:_ -> <<>> end.
33 |
34 | secret() -> application:get_env(n2o,secret,<<"ThisIsClassified">>).
35 | hex(Bin) -> << << (digit(A1)),(digit(A2)) >> || <> <= Bin >>.
36 | unhex(Hex) -> << << (erlang:list_to_integer([H1,H2], 16)) >> || <> <= Hex >>.
37 | digit(X) when X >= 0 andalso X =< 9 -> X + 48;
38 | digit(X) when X >= 10 andalso X =< 15 -> X + 87.
39 | sid(Seed) -> hex(binary:part(mac([application:get_env(n2o,hmac,sha256),secret(),term_to_binary(Seed)]),0,10)).
40 |
--------------------------------------------------------------------------------
/src/services/n2o_session.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_session).
2 | -compile([nowarn_unused_function]).
3 | -include_lib("stdlib/include/ms_transform.hrl").
4 | -description('N2O Session').
5 | -export([authenticate/2, get_value/3, set_value/3, storage/0, prolongate/0, from/1, ttl/0, till/2]).
6 | -export([clear/1, delete/1, update/1, lookup/1, invalidate_sessions/0]).
7 |
8 | % PRELUDE
9 |
10 | part(X) -> binary:part(X,0,10).
11 | to(X) -> calendar:datetime_to_gregorian_seconds(X).
12 | from(X) -> calendar:gregorian_seconds_to_datetime(X).
13 | cut(Bin) -> binary:part(Bin,0,20).
14 | expired(Till) -> Till < to(calendar:local_time()).
15 | expire() -> to(till(calendar:local_time(), ttl())).
16 | auth(Sid,Exp) -> {{Sid,'auth'},{Exp,[]}}.
17 | storage() -> application:get_env(n2o,session_storage,n2o_session).
18 | token(A) -> (storage()):update(A), {'Token',n2o:pickle(A)}.
19 | token(A,P) -> (storage()):update(A), {'Token',P}.
20 | ttl() -> application:get_env(n2o,ttl,60*15).
21 | till(Now,TTL) ->
22 | case is_atom(TTL) of
23 | true -> TTL;
24 | false -> from(to(Now)+TTL)
25 | end.
26 | prolongate() -> application:get_env(n2o,nitro_prolongate,false).
27 | sid(Seed) -> n2o_secret:sid(Seed).
28 |
29 | % API
30 |
31 | new() -> token(auth(sid(os:timestamp()),expire())).
32 |
33 | authenticate([], Pickle) ->
34 | case n2o:depickle(Pickle) of
35 | <<>> -> token(auth(sid(os:timestamp()),expire()));
36 | {{Sid,'auth'},{Till,[]}} = Auth ->
37 | case {expired(Till), prolongate()} of
38 | {false,false} -> token(Auth,Pickle);
39 | {false,true} -> move(Sid), token(auth(Sid,expire()));
40 | {true,false} -> (storage()):delete({Sid,auth}), new();
41 | {true,true} -> move(Sid), (storage()):delete({Sid,auth}), new()
42 | end;
43 | _ -> new()
44 | end.
45 |
46 | get_value(Session, Key, Default) ->
47 | case (storage()):lookup({Session,Key}) of
48 | [] -> Default;
49 | {{Session,Key},{Till,Value}} -> case expired(Till) of
50 | false -> Value;
51 | true -> (storage()):delete({Session,Key}), Default end end.
52 |
53 | set_value(Session, Key, Value) ->
54 | (storage()):update({{Session,Key},{expire(),Value}}), Value.
55 |
56 | move(Sid) ->
57 | [ (storage()):update({{Sid,Key},{expire(),Val}}) || {{_,Key},{_,Val}} <- ets:select(cookies,
58 | ets:fun2ms(fun(A) when (element(1,element(1,A)) == Sid) -> A end)) ], ok.
59 |
60 | clear(Session) ->
61 | [ ets:delete(cookies,X) || X <- ets:select(cookies,
62 | ets:fun2ms(fun(A) when (element(1,element(1,A)) == Session) -> element(1,A) end)) ], ok.
63 |
64 | lookup({Session,Key}) ->
65 | case ets:lookup(cookies,{Session,Key}) of [Value] -> Value; Values -> Values end.
66 |
67 | update(Token) ->
68 | ets:insert(cookies,Token).
69 |
70 | delete({Session,Key}) ->
71 | ets:delete(cookies,{Session,Key}).
72 |
73 | invalidate_sessions() ->
74 | ets:foldl(fun(X,A) -> {Sid,Key} = element(1,X),
75 | get_value(Sid,Key,[]), A end, 0, cookies),ok.
76 |
77 | % TESTS
78 |
79 | positive_test() ->
80 | application:set_env(n2o,nitro_prolongate,false),
81 | {'Token',B}=n2o_session:authenticate("",""),
82 | {{SID,Key},{Till,[]}} = n2o:depickle(B),
83 | {'Token',C}=n2o_session:authenticate("",B),
84 | {{SID,Key},{Till,[]}} = n2o:depickle(C),
85 | delete({SID,'auth'}),
86 | true=(C==B).
87 |
88 | negative_test() ->
89 | application:set_env(n2o,nitro_prolongate,false),
90 | application:set_env(n2o, ttl, 2),
91 | {'Token', TokenA} = n2o_session:authenticate("", ""),
92 | {{SID0,_},{_,[]}} = n2o:depickle(TokenA),
93 | timer:sleep(3000),
94 | {'Token', TokenB} = n2o_session:authenticate("", TokenA),
95 | {{SID1,_},{_,[]}} = n2o:depickle(TokenB),
96 | application:set_env(n2o, ttl, 60*15),
97 | TokenWasChanged = TokenA /= TokenB,
98 | {'Token', TokenC} = n2o_session:authenticate("", TokenB),
99 | {{SID2,_},{_,[]}} = n2o:depickle(TokenC),
100 | NewTokenIsValid = TokenB == TokenC,
101 | delete({SID0,auth}),
102 | delete({SID1,auth}),
103 | delete({SID2,auth}),
104 | TokenWasChanged == NewTokenIsValid.
105 |
106 | test_set_get_value() ->
107 | InputValue = base64:encode(crypto:strong_rand_bytes(8)),
108 | SID = sid(os:timestamp()),
109 | Key = base64:encode(crypto:strong_rand_bytes(8)),
110 | set_value(SID, Key, InputValue),
111 | ResultValue = get_value(SID, Key, []),
112 | delete({SID,Key}),
113 | InputValue == ResultValue.
114 |
--------------------------------------------------------------------------------
/src/services/n2o_xml.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_xml).
2 | -description('N2O XML Formatter').
3 | -include("n2o.hrl").
4 | -include_lib("xmerl/include/xmerl.hrl").
5 | -export([encode/1,decode/1]).
6 |
7 | str(Data) -> lists:flatten(xmerl:export_simple(Data, xmerl_xml)).
8 | zip(Cx,Data) -> lists:zip(Cx,tl(tuple_to_list(Data))).
9 |
10 | cx(Data) -> Cx = record_info(fields, cx), [{cx,zip(Cx,Data),[]}].
11 | pi(Data) -> Pi = record_info(fields, pi), [{pi,zip(Pi,Data),[]}].
12 |
13 | encode(#cx{}=C) -> str(cx(C));
14 | encode(#pi{}=C) -> str(pi(C)).
15 |
16 | decode(Bin) ->
17 | {#xmlElement{name=N,attributes=L},_} = xmerl_scan:string(Bin),
18 | list_to_tuple([N|[J||#xmlAttribute{value=J}<-L]]).
19 |
--------------------------------------------------------------------------------
/src/ws/n2o_cowboy.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_cowboy).
2 | -include_lib("n2o/include/n2o.hrl").
3 | -description('N2O Cowboy HTTP Backend').
4 | -export([init/2, websocket_init/1, websocket_handle/2, websocket_info/2, terminate/3, points/0]).
5 | -export([params/1, form/1, path/1, request_body/1, headers/1, header/3,
6 | response/2, reply/2, cookies/1, cookie/2, cookie/3, cookie/5, delete_cookie/2,
7 | peer/1, env/1, fix2/1, fix1/1]).
8 |
9 | % Cowboy 2 WebSocket API
10 |
11 | init(Req,_Opts) -> {cowboy_websocket, Req, Req}.
12 |
13 | ws({ok,_,S}) -> {ok,S};
14 | ws({reply,{binary,Rep},_,S}) -> {reply,{binary,Rep},S};
15 | ws({reply,{json,Rep},_,S}) -> {reply,{binary,n2o_json:encode(Rep)},S};
16 | ws({reply,{bert,Rep},_,S}) -> {reply,{binary,n2o_bert:encode(Rep)},S};
17 | ws({reply,{text,Rep},_,S}) -> {reply,{text,Rep},S};
18 | ws({reply,{default,Rep},_,S})-> {reply,{binary,n2o:encode(Rep)},S};
19 | ws({reply,{Encoder,Rep},_,S})-> {reply,{binary,Encoder:encode(Rep)},S};
20 | ws(X) -> ?LOG_ERROR(#{unknown=>X}), {shutdown,[]}.
21 |
22 | websocket_init(S) -> ws(n2o_proto:init([],S,[],ws)).
23 | websocket_handle(D,S) -> ws(n2o_proto:stream(D,[],S)).
24 | websocket_info(D,S) -> ws(n2o_proto:info(D,[],S)).
25 | terminate(M,R,S) -> ws(n2o_proto:info({direct,{exit,M}},R,S)).
26 |
27 | % Cowboy Bridge Abstraction
28 |
29 | params(Req) -> cowboy_req:qs_vals(Req).
30 | form(Req) -> {ok,Params,NewReq} = cowboy_req:body_qs(Req), {Params,NewReq}.
31 | path(Req) -> {Path,_NewReq} = cowboy_req:path(Req), Path.
32 | request_body(Req) -> cowboy_req:body(Req).
33 | headers(Req) -> cowboy_req:headers(Req).
34 | header(Name, Value, Req) -> cowboy_req:set_resp_header(Name, Value, Req).
35 | response(Html,Req) -> cowboy_req:set_resp_body(Html,Req).
36 | reply(StatusCode,Req) -> cowboy_req:reply(StatusCode, Req).
37 | cookies(Req) -> element(1,cowboy_req:cookies(Req)).
38 | cookie(Cookie,Req) -> element(1,cowboy_req:cookie(n2o:to_binary(Cookie),Req)).
39 | cookie(Cookie, Value, Req) -> cookie(Cookie,Value,<<"/">>,0,Req).
40 | cookie(Name, Value, Path, TTL, Req) ->
41 | Options = [{path, Path}, {max_age, TTL}],
42 | cowboy_req:set_resp_cookie(Name, Value, Options, Req).
43 | delete_cookie(Cookie,Req) -> cookie(Cookie,<<"">>,<<"/">>,0,Req).
44 | peer(Req) -> {{Ip,Port},Req} = cowboy_req:peer(Req), {Ip,Port}.
45 |
46 | fix1({error,bad_name}) -> "priv";
47 | fix1(X) -> case filelib:is_dir(X) of true -> X; _ -> fix1({error,bad_name}) end.
48 | fix2({error,bad_name}) -> "deps/n2o/priv";
49 | fix2(X) -> case filelib:is_dir(X) of true -> X; _ -> fix2({error,bad_name}) end.
50 |
51 | static() -> { dir, fix1(code:priv_dir(application:get_env(n2o,app,review)))++"/static", mime() }.
52 | n2o() -> { dir, fix2(code:priv_dir(n2o)), mime() }.
53 | favico() -> { file, code:priv_dir(application:get_env(n2o,app,review))++"/static/favicon.ico", mime() }.
54 | mime() -> [ { mimetypes, cow_mimetypes, all } ].
55 | port() -> application:get_env(n2o,port,8000).
56 |
57 | points() -> cowboy_router:compile([{'_', [
58 | { "/favicon.ico", cowboy_static, favico() },
59 | { "/n2o/[...]", cowboy_static, n2o() },
60 | { "/app/[...]", cowboy_static, static() },
61 | { "/ws/[...]", n2o_cowboy, [] } ]}]).
62 |
63 | env(App) -> [{port, port()},
64 | {certfile, code:priv_dir(App)++"/ssl/fullchain.pem"},
65 | {keyfile, code:priv_dir(App)++"/ssl/privkey.pem"},
66 | {cacertfile, code:priv_dir(App)++"/ssl/fullchain.pem"}].
67 |
68 |
--------------------------------------------------------------------------------
/src/ws/n2o_gproc.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_gproc).
2 | -description('N2O GPROC MQ Backend').
3 | -include_lib("n2o/include/n2o.hrl").
4 | -export(?MESSAGE_API).
5 |
6 | init() -> ok.
7 | send(Pool, Message) -> gproc:send({p,l,Pool},Message).
8 | reg(Pool) -> reg(Pool,undefined).
9 | reg(Pool, Value) ->
10 | case get({pool,Pool}) of
11 | undefined -> gproc:reg({p,l,Pool},Value), put({pool,Pool},Pool);
12 | _Defined -> skip end.
13 | unreg(Pool) ->
14 | case get({pool,Pool}) of
15 | undefined -> skip;
16 | _Defined -> gproc:unreg({p,l,Pool}), erase({pool,Pool}) end.
17 |
--------------------------------------------------------------------------------
/src/ws/n2o_static.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_static).
2 | -export([init/2,endpoints/2, index/0, websocket_info/2, websocket_init/1, websocket_handle/2,
3 | malformed_request/2, forbidden/2, content_types_provided/2, ranges_provided/2,
4 | resource_exists/2, last_modified/2, generate_etag/2, get_file/2]).
5 |
6 | malformed_request(R,S) -> cowboy_static:malformed_request(R,S).
7 | forbidden(R,S) -> cowboy_static:forbidden(R,S).
8 | content_types_provided(R,S) -> cowboy_static:content_types_provided(R,S).
9 | ranges_provided(R,S) -> cowboy_static:ranges_provided(R,S).
10 | resource_exists(R,S) -> cowboy_static:resource_exists(R,S).
11 | last_modified(R,S) -> cowboy_static:last_modified(R,S).
12 | generate_etag(R,S) -> cowboy_static:generate_etag(R,S).
13 | get_file(R,S) -> cowboy_static:get_file(R,S).
14 | websocket_init(S) -> n2o_cowboy:websocket_init(S).
15 | websocket_handle(D,S) -> n2o_cowboy:websocket_handle(D,S).
16 | websocket_info(D,S) -> n2o_cowboy:websocket_info(D,S).
17 |
18 | index() -> [n2o_cowboy:fix1(code:priv_dir(?MODULE)), "/static/index.html"].
19 |
20 | init(#{headers := #{<<"upgrade">> := <<"websocket">>}} = Req, _) -> {cowboy_websocket, Req, Req};
21 | init(Req, _) ->
22 | Index = filename:absname(iolist_to_binary(index())),
23 | {ok, Info} = file:read_file_info(Index, [{time, universal}]),
24 | {cowboy_rest, Req, {Index, {direct, Info}, []}}.
25 |
26 | endpoints(App,Module) ->
27 | S = {dir, n2o_cowboy:fix1(code:priv_dir(App))++"/static", []},
28 | cowboy_router:compile([{'_', [{"/", Module, []},
29 | {"/app/[...]", cowboy_static, S},
30 | {"/[...]", cowboy_static, S}]}]).
31 |
--------------------------------------------------------------------------------
/src/ws/n2o_syn.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_syn).
2 | -description('N2O SYN MQ Backend').
3 | -include("n2o.hrl").
4 | -export(?MESSAGE_API).
5 |
6 | init() -> ok.
7 | send(Pool, Message) -> syn:publish(term_to_binary(Pool),Message).
8 | reg(Pool) -> reg(Pool,undefined).
9 | reg(Pool, _Value) ->
10 | case get({pool,Pool}) of
11 | undefined -> syn:join(term_to_binary(Pool),self()),
12 | put({pool,Pool},Pool);
13 | _Defined -> skip end.
14 | unreg(Pool) ->
15 | case get({pool,Pool}) of
16 | undefined -> skip;
17 | _Defined -> syn:leave(term_to_binary(Pool), self()),
18 | erase({pool,Pool}) end.
19 |
--------------------------------------------------------------------------------
/src/ws/n2o_ws.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_ws).
2 | -include_lib("n2o/include/n2o.hrl").
3 | -export([send/2, proc/2]).
4 |
5 | send(C,M) -> C ! M.
6 |
7 | proc(init,#pi{name=Name}=Async) -> n2o:reg(Name), {ok,Async#pi{state=application:get_env(n2o,proto,n2o_proto)}};
8 |
9 | proc({ring, _Topic, Publish}, State) -> proc(Publish, State);
10 |
11 | proc({publish, C, Token, Request}, State = #pi{name=Server,state=Module}) ->
12 | Ctx = #cx { session= n2o:to_binary(Token), node=Server,
13 | client_pid=C, state=application:get_env(kvs,dba,[]) },
14 | put(context, Ctx),
15 | Return = case n2o_proto:try_info(Module,Request,[],Ctx,n2o_proto:protocols()) of
16 | {reply,{_, <<>>},_,_} -> skip;
17 | {reply,{text, Text},_,_} -> {ok,send(C,{flush,Text})};
18 | {reply,{bert, Term},_,_} -> {ok,send(C,n2o_bert:encode(Term))};
19 | {reply,{json, Term},_,_} -> {ok,send(C,n2o_json:encode(Term))};
20 | {reply,{binary, Term},_,_} -> {ok,send(C,Term)};
21 | {reply,{default,Term},_,_} -> {ok,send(C,n2o:encode(Term))};
22 | {reply,{Encoder,Term},_,_} -> {ok,send(C,Encoder:encode(Term))};
23 | Reply -> {error,{"Invalid Return",Reply}} end,
24 | {reply, Return, State};
25 |
26 | proc(Unknown,#pi{name=Name}=Async) ->
27 | io:format("WS ~p: ~p~n",[Name,Unknown]),
28 | {reply,{unknown,Unknown,0},Async}.
29 |
--------------------------------------------------------------------------------
/sys.config:
--------------------------------------------------------------------------------
1 | [{n2o,[{app,n2o},
2 | {ws_services,["chat","crm"]},
3 | {mqtt_services,{chat,[]}},
4 | {upload,"./priv/static/"},
5 | {mq,n2o_syn},
6 | {protocols,[]},
7 | {session,n2o_session},
8 | {pickler,n2o_pickle}]}].
9 |
--------------------------------------------------------------------------------
/test/bert.sh:
--------------------------------------------------------------------------------
1 | ./bert_gen.erl > bert.data
2 | node bert_test.js
3 |
--------------------------------------------------------------------------------
/test/bert_gen.erl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env escript
2 |
3 | -module(genbert).
4 |
5 | -compile([export_all]).
6 |
7 | object(D) ->
8 | case crypto:rand_uniform(0, 5) of
9 | 0 -> tuple(D);
10 | 1 -> bin(D);
11 | 2 -> list(D);
12 | 3 -> bytes(D);
13 | 4 -> atom(D)
14 | end.
15 |
16 | uni(1) -> rnd(0, 16#7F);
17 | uni(2) -> rnd(16#80, 16#7FF);
18 | uni(3) -> [rnd(16#800, 16#D7FF), rnd(16#E000, 16#FFFD)];
19 | uni(4) -> rnd(16#10000, 16#10FFFF).
20 |
21 | utf8() -> [uni(X) || X <- lists:seq(1, 3)].
22 |
23 | unicode(0, Acc) -> lists:flatten(Acc);
24 | unicode(N, Acc) -> unicode(N - 1, [utf8() | Acc]).
25 |
26 | size() -> 20.
27 |
28 | rnd(A) -> rnd(1, A).
29 |
30 | rnd(L, H) -> crypto:rand_uniform(L, H).
31 |
32 | list(2) -> [];
33 | list(D) ->
34 | [object(D + 1) || _ <- lists:seq(1, size() - D)].
35 |
36 | tuple(D) -> list_to_tuple(list(D)).
37 |
38 | bin(D) -> list_to_binary(bytes(D)).
39 |
40 | bytes(_) -> latin(rnd(size()), []).
41 |
42 | atom(D) -> list_to_atom(bytes(D)).
43 |
44 | latin(0, A) -> A;
45 | latin(N, A) -> latin(N - 1, [rnd(size()) + 96 | A]).
46 |
47 | main(_) ->
48 | [io:format("~w\n",
49 | [binary_to_list(term_to_binary(tuple(0)))])
50 | || _ <- lists:seq(1, size())].
51 |
--------------------------------------------------------------------------------
/test/bert_test.js:
--------------------------------------------------------------------------------
1 |
2 | var bert = require('../priv/protocols/bert.js');
3 | var utf8 = require('../priv/utf8.js');
4 | var fs = require('fs');
5 |
6 | utf8_dec = utf8.dec;
7 | utf8_toByteArray = utf8.enc;
8 |
9 | print = function (x) { return "["+Array.apply([], x ).join(",")+"]"; }
10 | pass = true;
11 | counter = 0;
12 |
13 | fs.readFileSync('bert.data').toString().split('\n').forEach(function (data) {
14 | if (data == "") return;
15 | pass = pass && (data==print(bert.enc(bert.dec(new Uint8Array(JSON.parse(data)).buffer))));
16 | if (pass) { console.log("OK: "+counter); }
17 | counter+=1;
18 | if (!pass) {
19 | console.log(data);
20 | console.log(print(bert.enc(bert.dec(new Uint8Array(JSON.parse(data)).buffer))));
21 | console.log(bert.dec(new Uint8Array(JSON.parse(data)).buffer));
22 | console.log("ERROR: "+data);
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/test/casper/casper.js:
--------------------------------------------------------------------------------
1 | casper.test.begin("Basic connectivity", 1, function (test)
2 | {
3 | test.info("Connecting")
4 | casper
5 | .start('http://localhost:8000/static/spa/login.htm')
6 | .then(function ()
7 | {
8 | test.assertHttpStatus(200)
9 | test.info('Loggin')
10 | this.click('#loginButton')
11 | }).waitForSelector('#upload', function()
12 | {
13 | test.info('Logged In.')
14 | })
15 | .run(function ()
16 | {
17 | test.done()
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/test/elements.erl:
--------------------------------------------------------------------------------
1 | -module(elements).
2 |
3 | -include_lib("n2o/include/wf.hrl").
4 |
5 | -include_lib("common_test/include/ct.hrl").
6 |
7 | -compile(export_all).
8 |
9 | main() ->
10 | Title = wf_render_elements:render_elements(title()),
11 | Body = wf_render_elements:render_elements(body()),
12 | #dtl{file = "index.html", app = n2o, folder = "test",
13 | bindings = [{title, Title}, {body, Body}]}.
14 |
15 | title() -> "N2O Test".
16 |
17 | body() ->
18 | [#label{body = "test label"},
19 | #textbox{body = "test textbox"}].
20 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{title}}
4 |
5 |
6 | {{body}}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/test/n2o_SUITE.erl:
--------------------------------------------------------------------------------
1 | -module(n2o_SUITE).
2 |
3 | -behaviour(cowboy_http_handler).
4 |
5 | -include_lib("common_test/include/ct.hrl").
6 |
7 | -include_lib("n2o/include/wf.hrl").
8 |
9 | -compile(export_all).
10 |
11 | suite() -> [{timetrap, {seconds, 30}}].
12 |
13 | all() -> [{group, elements}].
14 |
15 | groups() -> [{elements, [], [elements]}].
16 |
17 | init_per_suite(Config) ->
18 | ok = application:start(crypto),
19 | ok = application:start(ranch),
20 | ok = application:start(cowboy),
21 | Config.
22 |
23 | end_per_suite(_Config) ->
24 | application:stop(cowboy),
25 | application:stop(ranch),
26 | application:stop(crypto),
27 | ok.
28 |
29 | init_per_group(elements, Config) ->
30 | Port = 33084,
31 | Transport = ranch_tcp,
32 | {ok, _} = cowboy:start_http(elements,
33 | 100,
34 | [{port, Port}],
35 | [{env, [{dispatch, init_dispatch()}]},
36 | {max_keepalive, 50},
37 | {timeout, 500}]),
38 | {ok, Client} = cowboy_client:init([]),
39 | [{scheme, <<"http">>},
40 | {port, Port},
41 | {opts, []},
42 | {transport, Transport},
43 | {client, Client}
44 | | Config].
45 |
46 | end_per_group(Name, _) ->
47 | cowboy:stop_listener(Name),
48 | ok.
49 |
50 | init_dispatch() ->
51 | cowboy_router:compile([{"localhost",
52 | [{'_', n2o_SUITE, []}]}]).
53 |
54 | build_url(Path, Config) ->
55 | {scheme, Scheme} = lists:keyfind(scheme, 1, Config),
56 | {port, Port} = lists:keyfind(port, 1, Config),
57 | PortBin = list_to_binary(integer_to_list(Port)),
58 | PathBin = list_to_binary(Path),
59 | <>.
61 |
62 | elements(Config) ->
63 | Client = (?config(client, Config)),
64 | URL = build_url("/elements", Config),
65 | ct:log("-> url ~p", [URL]),
66 | {ok, Client2} = cowboy_client:request(<<"GET">>,
67 | URL,
68 | Client),
69 | {ok, 200, Headers, Client3} =
70 | cowboy_client:response(Client2),
71 | {ok, Body, _} = cowboy_client:response_body(Client3),
72 | ct:log("-> response Body: ~p", [Body]),
73 | {_, 10} = binary:match(Body, <<"test label">>),
74 | {_, 12} = binary:match(Body, <<"test textbox">>),
75 | ok.
76 |
77 | %% handle to process http request
78 | -record(state, {headers, body}).
79 |
80 | init({_Transport, http}, Req, _Opts) ->
81 | {ok, Req, #state{}}.
82 |
83 | handle(Req, State) ->
84 | RequestBridge =
85 | simple_bridge:make_request(cowboy_request_bridge, Req),
86 | ResponseBridge =
87 | simple_bridge:make_response(cowboy_response_bridge,
88 | RequestBridge),
89 | wf_context:init_context(RequestBridge, ResponseBridge),
90 | %% wf_handler:set_handler(http_basic_auth_security_handler, n2o_auth),
91 | {ok, NewReq} = wf_core:run(),
92 | {ok, NewReq, State}.
93 |
94 | terminate(_Reason, _Req, _State) -> ok.
95 |
--------------------------------------------------------------------------------
/test/test.hrl:
--------------------------------------------------------------------------------
1 | -define(APP, n2o).
2 | -define(TEMPLATE, filename:join(lists:reverse(tl(lists:reverse(filename:split(code:priv_dir(?APP)))))) ++ "/test/index.html").
3 |
--------------------------------------------------------------------------------