├── .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 | [![Actions Status](https://github.com/synrc/n2o/workflows/mix/badge.svg)](https://github.com/synrc/n2o/actions) 4 | [![Hex pm](http://img.shields.io/hexpm/v/n2o.svg?style=flat)](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 | 30 |
31 | 32 |

N2O

33 |
34 | 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 |
    147 |
  • utf8.js — UTF-8 encoder/decoder
  • 148 |
  • zlib.js — ZLIB inflate algorithm
  • 149 |
  • ieee754.js — IEEE-754 encoder/decoder
  • 150 |
  • bert.js — BERT encoder/decoder
  • 151 |
  • n2o.js — N2O protocol looper
  • 152 |
  • heart.js — HEART protocol
  • 153 |
  • nitro.js — NITRO protocol
  • 154 |
  • ftp.js — FTP protocol
  • 155 |
  • mq.js — MQTT client
  • 156 |
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 | 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 |
59 |

You may also want to read: 60 | bert.js, 61 | utf8.js, 62 | mq.js, 63 | nitro.js, 64 | n2o.js. 65 |

66 |
67 |
68 |
69 | 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 | 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 |
86 |

You may also want to read: 87 | bert.js, 88 | utf8.js, 89 | mq.js, 90 | nitro.js, 91 | n2o.js. 92 |

93 |
94 |
95 |
96 | 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 | 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 |
52 |

You may also want to read: 53 | bert.js, 54 | heart.js, 55 | n2o.js. 56 |

57 |
58 |
59 |
60 | 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 | 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 |
44 |

You may also want to read: 45 | bert.js, 46 | utf8.js, 47 | n2o.js. 48 |

49 |
50 |
51 |
52 | 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 | 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 |
330 |

This module may refer to: 331 | n2o_pi, 332 | n2o_auth, 333 | n2o_stream, 334 | n2o_mqtt, 335 | n2o_proto. 336 |

337 |
338 |
339 |
340 | 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 | 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 |
59 |

You may also want to read: 60 | utf8.js, 61 | heart.js, 62 | nitro.js, 63 | bert.js. 64 |

65 |
66 |
67 |
68 | 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 | 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 |
69 |

This module may refer to: 70 | n2o, 71 | n2o_proto, 72 | n2o_mqtt. 73 |

74 |
75 |
76 |
77 | 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 | 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 | 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 | 31 |
32 | 33 |

COWBOY

34 |
35 |
36 |
37 |
38 |

INTRO

39 |

The n2o_cowboy 40 | module provides COWBOY API interface connectivity.

41 |
42 |
43 |

You may also want to read: 44 | n2o_heart, 45 | n2o_nitro. 46 |

47 |
48 |
49 |
50 | 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 | 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 |
61 |

You may also want to read: 62 | n2o_heart, 63 | mq.js, 64 | n2o_nitro. 65 |

66 |
67 |
68 |
69 | 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 | 31 |
32 | 33 |

GPROC

34 |
35 |
36 |
37 |
38 |

INTRO

39 |

The n2o_gproc 40 | module provides GPROC MQ backend.

41 |
42 |
43 |

You may also want to read: 44 | n2o_heart, 45 | n2o_nitro. 46 |

47 |
48 |
49 |
50 | 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 | 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 |
63 |

You may also want to read: 64 | n2o_ftp, 65 | n2o_nitro. 66 |

67 |
68 |
69 |
70 | 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 | 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 | 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 | 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 |
89 |

This module may refer to: 90 | n2o, 91 | n2o_pi, 92 | n2o_proto. 93 |

94 |
95 |
96 |
97 | 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 | 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 |
216 |

This module may refer to: 217 | n2o, 218 | n2o_proto, 219 | n2o_mqtt. 220 |

221 |
222 |
223 |
224 | 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 | 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 | 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 | 31 |
32 | 33 |

RING

34 |
35 |
36 |
37 |
38 |

INTRO

39 |

The n2o_ring 40 | module provides hash ring for virtual nodes.

41 |
42 |
43 |

This module may refer to: 44 | n2o, 45 | n2o_proto, 46 | n2o_mqtt. 47 |

48 |
49 |
50 |
51 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 31 |
32 | 33 |

SYN

34 |
35 |
36 |
37 |
38 |

INTRO

39 |

The n2o_syn 40 | module provides SYN MQ backend.

41 |
42 |
43 |

You may also want to read: 44 | n2o_heart, 45 | n2o_nitro. 46 |

47 |
48 |
49 |
50 | 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 | 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 | 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 | 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 |
54 |

You may also want to read: 55 | bert.js, 56 | heart.js, 57 | nitro.js, 58 | n2o.js. 59 |

60 |
61 |
62 |
63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /man/zlib.js.htm: -------------------------------------------------------------------------------- 1 | 2 | FTP.JS 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
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 |
36 | 37 |

You may also want to read: 38 | 39 | bert.js, 40 | utf8.js, 41 | mq.js, 42 | nitro.js, 43 | n2o.js. 44 |

45 | 46 |
47 |
48 |
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 | --------------------------------------------------------------------------------