├── .gitignore
├── LICENSE
├── README-tr.md
├── README.md
├── develop
├── client.js
├── database.js
├── events
│ ├── exampleEvent.js
│ ├── index.js
│ └── readDatabaseEvent.js
└── index.html
├── index.js
├── package.json
└── src
├── bootNodes.js
├── bridge.js
├── database.js
├── events.js
├── events
├── answer.js
├── candidate.js
├── drop.js
└── offer.js
├── signalling.js
└── utils
├── dataPool.js
├── index.js
├── node.js
├── nodeId.js
└── sigStore.js
/.gitignore:
--------------------------------------------------------------------------------
1 | package-lock.json
2 | node_modules/
3 | dist/
4 | .cache/
5 | .parcel-cache
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 FoxQL
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README-tr.md:
--------------------------------------------------------------------------------
1 | # FoxQL
2 |
3 | Foxql, web2 teknolojileri ile merkezi olmayan uygulamalar geliştirmenizi sağlar.
4 |
5 | - Doğası gereği foxql ile geliştirdiğiniz uygulamalar oldukça kaotik bir çalışma şekline sahip olacaklar.
6 | - Bir verinin varlığı, veriyi sahiplenen düğümün ulaşılabilir olmasına bağlıdır. Bu ağda sorduğunuz sorunun cevabının t anında varlığını sorgulayabildiğiniz anlamına gelir.
7 | - Verilerin varlığının kesin olarak kanıtlanabilmesi için düğümlerinize herhangi bir web3 blok zincirinde çıkartacağınız tokenları ödül olarak dağıtabilirsiniz.
8 | - FoxQL sinyalleşme aşamasında görece olarak merkezidir. Bu önemli değil çünkü sinyalleşme oldukça ucuz bir çözüm. Siz istediğiniz sürece ağda iletişim her zaman devam edecektir.
9 | - Yaptığınız her eylem rastgele bir hash sorusu üretecektir. Cevabı dinlediğiniz süre ve seçilebilir nonce aralığı dinlediğiniz etkinliğin zorluğunu belirleyecektir.
10 | - FoxQL gerekmedikçe diğer düğümler ile etkileşime girmez, etkileşime girdiği düğümler ile işi bittiğinde bağlantıları öldürür ve yenilerini beklemeye devam eder. Bu webRTC tarafında bazı performans ve sınırlandırmaların önemi olmadığı anlamına gelir.
11 | - Son kullanıcı dilediği taktirde düğümü ile alakalı olan tüm bilgileri farklı bir platforma geçirebilir. Bu geliştiricilerin son kullanıcıyı rahatsız edebilecek kararlar almasını engeller.
12 | - FoxQL hiç bir etkinliğin kaydını tutmaz, düğümler arasında gerçekleşen veri alışverişini takip etmez. Sadece soruların diğer düğümlere iletilmesini ve yeni bir bağlantı yarışının başlamasını sağlar
13 |
14 | ## Kurulum
15 | [npm](https://www.npmjs.com)
16 |
17 | ```bash
18 | npm i @foxql/foxql-peer
19 | ```
20 |
21 | ## Kurulum
22 |
23 | ```javascript
24 | import foxql from '@foxql/foxql-peer';
25 |
26 | const node = new foxql({
27 | maxNodeCount: 30, // Aktif bağlantı limiti
28 | maxCandidateCallTime: 2000, // Düğüm adayları için sorulan soru kaç milisaniye dinlenmeli?
29 | powPoolingTime: 1000, // Soru çözümü ne kadar sürecek?
30 | dappAlias: 'demo-app'
31 | })
32 |
33 |
34 | ```
35 |
36 | ### Websocket Ayarlarını Değiştirin
37 |
38 | ```js
39 | import foxql from "@foxql/foxql-peer";
40 |
41 | const node = new foxql({
42 | dappAlias: 'demo-app',
43 | wssOptions: {
44 | transport: ['websocket'],
45 | jsonp: false
46 | }
47 | });
48 | ```
49 |
50 | ### Düğüm meta bilgileri
51 | ```javascript
52 | node.setMetaData({
53 | name: 'test-node',
54 | description: 'test-desc'
55 | })
56 | ```
57 |
58 | ### Etkinlik tanımı
59 | Düğüm etkinlikleri P2P bağlantının sağlanabilmesi ve veri alışverişi için kullanılır. Her etkinlik iki farklı aşamadan meydana gelir. Simulate durumunun true gelmesi etkinliğin webSocket aracılığı ile çağırıldığını belirtir.
60 |
61 | Simulate durumunun pozitif olduğu durumlarda yeni bir webRTC bağlantısına aday olup olmayacağınıza karar vermelisiniz.
62 |
63 | ```javascript
64 | node.on('hello-world', async ({sender, message}, simulate = false)=>{
65 | if(simulate) {
66 | console.log('Simulate state')
67 | // work on proof case
68 | return true // accept webRTC connection.
69 | }
70 | // webRTC
71 | this.reply(sender, {
72 | hi: this.nodeId
73 | })
74 | })
75 | ```
76 |
77 | ### Düğüm keşfi
78 | ```javascript
79 | async function broadcast(){
80 | const answer = await node.ask({
81 | transportPackage: {
82 | p2pChannelName: 'hello-world',
83 | message: 'Hello world'
84 | }
85 | })
86 | return answer
87 | }
88 | ```
89 | ### Yapışkan düğümler
90 | stickyNode seçeneği bağlantıların karşılıklı sabitlenmesini sağlar. FoxQL varsayılan olarak her keşiften sonra bağlantıları öldürür.
91 | ```javascript
92 | node.ask({
93 | transportPackage: {
94 | p2pChannelName: 'hello-world',
95 | message: 'Hello world'
96 | },
97 | stickyNode: true
98 | })
99 | ```
100 |
101 | ### Yerel Keşif
102 | Ağ üzerinde sorduğunuz her keşif sorusu varsayılan olarak bağlantılı olduğunuz tüm düğümlere tekrar sorulur. Bazı spesifik durumlar için soruyu sadece yeni adaylara yönlendirebilirsiniz. Bu düğüm trafiğinizi azaltmanızı sağlar.
103 | ```javascript
104 | node.ask({
105 | transportPackage: {
106 | p2pChannelName: 'hello-world',
107 | message: 'Hello world'
108 | },
109 | localWork: true
110 | })
111 | ```
112 |
113 | ### Düğümü Başlatın
114 | ```javascript
115 | node.start()
116 | ```
117 |
118 | ## Katkıda bulun
119 | PullRequest açarak eklenmesini istediğiniz yeni özellikleri tartışabiliriz. Fark edeceğiniz üzere doküman Türkçe hazırlandı, ilk iş olarak bunu çevirmeye başlayabilirsiniz.
120 |
121 | ## License
122 | [MIT](https://github.com/foxql/peer/blob/main/LICENSE)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FoxQL
2 |
3 | FoxQL allows you to create decentralized applications using WEB2 technologies.
4 |
5 | - FoxQL applications have a chaotic environment by its nature.
6 | - The availabilty of the data depends on the accessbility of the node. In other words, the query may happen at _t_ time.
7 | - You can use tokens as a reward to proof the accessbility and availabilty of the data.
8 | - FoxQL is _relatively_ centralized in the process of signaling
9 | . Since signaling is a cheap solution, it's not a big deal. Communication does happen as long as you want.
10 | - Every action you take is going to produce a new hash query. The time you take to answer the question and cryptographic nonce is going to determine the difficulty of the action.
11 | - FoxQL doesn't interact with other nodes unless it is needed. Nodes terminate the connection when they are done, and wait for the next connection.This means performance issues and limitations of WebRTC doesn't matter.
12 | - End-user can transfer their data to other platforms at will. This ensures that platform owners can't affect them badly.
13 | - FoxQL doesn't store any log. It does not track any data transaction between nodes. It only ensures that queries are forwarded to other nodes and a new connection race begins.
14 |
15 | ## Installation
16 |
17 | ```js
18 | import foxql from "@foxql/foxql-peer";
19 |
20 | const node = new foxql({
21 | maxNodeCount: 30, // max connection limit
22 | maxCandidateCallTime: 2000, // how long to wait for a response from a candidate node
23 | powPoolingTime: 1000,
24 | dappAlias: 'demo-app'
25 | });
26 | ```
27 |
28 | ### Change Websocket Options
29 |
30 | ```js
31 | import foxql from "@foxql/foxql-peer";
32 |
33 | const node = new foxql({
34 | dappAlias: 'demo-app',
35 | wssOptions: {
36 | transport: ['websocket'],
37 | jsonp: false
38 | }
39 | });
40 | ```
41 |
42 |
43 | ### Node meta data
44 |
45 | ```js
46 | node.setMetaData({
47 | name: "test-node",
48 | description: "test-desc",
49 | });
50 | ```
51 |
52 | ### Event Definition
53 |
54 | Event definition is used to establish p2p communication and data transactions. Every event has two phases. _simulate_ means that the event is fetched over webSocket.
55 |
56 | ```js
57 | node.on("hello-world", async ({ sender, message }, simulate = false) => {
58 | if (simulate) {
59 | console.log("Simulate state");
60 | // work on proof case
61 | return true; // accept webRTC connection.
62 | }
63 | // webRTC
64 | this.reply(sender, {
65 | hi: this.nodeId,
66 | });
67 | });
68 | ```
69 |
70 | ### Node Discovery
71 |
72 | ```js
73 | async function broadcast() {
74 | const answer = await node.ask({
75 | transportPackage: {
76 | p2pChannelName: "hello-world",
77 | message: "Hello world",
78 | },
79 | });
80 | return answer;
81 | }
82 | ```
83 |
84 | ### Local Discovery
85 |
86 | By default, every discovery query you make on the network is forwarded again to all nodes you are connected to. In some cases, you can only redirect the queries to new candidate nodes. This node reduces the traffic.
87 |
88 | ```js
89 | node.ask({
90 | transportPackage: {
91 | p2pChannelName: "hello-world",
92 | message: "Hello world",
93 | },
94 | localWork: true,
95 | });
96 | ```
97 |
98 | ### Sticky Nodes
99 |
100 | _stickyNode_ option is used to establish constant connection between nodes. This option is useful when you want to establish a permanent connection between nodes.
101 | FoxQL will terminate the connection after every discovery.
102 |
103 | ```js
104 | node.ask({
105 | transportPackage: {
106 | p2pChannelName: "hello-world",
107 | message: "Hello world",
108 | },
109 | stickyNode: true,
110 | });
111 | ```
112 |
113 | ### Starting a Node
114 |
115 | ```js
116 | node.start();
117 | ```
118 |
119 | ## Contribute
120 |
121 | If you'd like to contribute, please fork the repository and make changes as you'd like. Pull requests are warmly welcome.
122 |
123 | ## Lisence
124 |
125 | [MIT](https://github.com/foxql/peer/blob/main/LICENSE)
126 |
--------------------------------------------------------------------------------
/develop/client.js:
--------------------------------------------------------------------------------
1 | import network from '../index.js';
2 | import events from './events'
3 | import * as dbConfig from './database'
4 |
5 | const p2p = new network({
6 | maxNodeCount: 30,
7 | maxCandidateCallTime: 1000, // ms
8 | powPoolingtime: 500,
9 | dappAlias: 'test-dapp'
10 | })
11 |
12 | p2p.setMetaData({
13 | name: 'test-node',
14 | description: 'test-desc'
15 | })
16 |
17 | p2p.loadEvents(events)
18 |
19 | p2p.start()
20 |
21 | window.testPOW = async ()=> {
22 | const answer = await p2p.ask({
23 | transportPackage: {
24 | p2pChannelName: 'give-me-your-name',
25 | message: 'Hello world'
26 | },
27 | livingTime: 1500,
28 | stickyNode: true,
29 | localWork: false
30 | })
31 | console.log(answer)
32 | }
33 |
34 | window.addEntry = ()=> {
35 | const transaction = p2p.indexedDb.transaction('entrys', 'readwrite')
36 | const store = transaction.objectStore('entrys')
37 | store.put({
38 | content: 'My first content in stored indexedDB',
39 | id: 1
40 | })
41 |
42 | transaction.oncomplete = (e => {
43 | console.log(e)
44 | })
45 | }
46 |
47 | window.p2p = p2p;
--------------------------------------------------------------------------------
/develop/database.js:
--------------------------------------------------------------------------------
1 | export const version = 2
2 |
3 | export function onerror(e)
4 | {
5 | console.error("An error occurred with IndexedDB")
6 | console.error(e)
7 | }
8 |
9 | export function onupgradeneeded(){
10 | //1
11 | const db = this.request.result;
12 | //2
13 | const store = db.createObjectStore("entrys", { keyPath: "id" })
14 |
15 | //3
16 | store.createIndex("content", ["content"], { unique: true })
17 | }
--------------------------------------------------------------------------------
/develop/events/exampleEvent.js:
--------------------------------------------------------------------------------
1 | export const listenerName = 'give-me-your-name'
2 |
3 | export async function listener({reply, sender}, simulate = false)
4 | {
5 | if(simulate) {
6 | console.log('Simulate state')
7 | // work on proof case
8 | return true
9 | }
10 |
11 | this.reply(sender, {
12 | my_name: 'bora' + this.nodeId
13 | })
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/develop/events/index.js:
--------------------------------------------------------------------------------
1 | import * as helloWorld from './exampleEvent.js'
2 | import * as readDatabase from './readDatabaseEvent.js'
3 | export default [
4 | helloWorld,
5 | readDatabase
6 | ]
--------------------------------------------------------------------------------
/develop/events/readDatabaseEvent.js:
--------------------------------------------------------------------------------
1 | export const listenerName = 'get-entry-by-id'
2 |
3 | export async function listener({reply, id}, simulate = false)
4 | {
5 | console.log(id, 'aranan id')
6 | if(simulate) {
7 | console.log('Simulate state')
8 | // work on proof case
9 | return true
10 | }
11 |
12 | const transaction = this.indexedDb.transaction('entrys', 'readwrite')
13 | const store = transaction.objectStore('entrys')
14 | const request = await store.get(id)
15 | request.onsuccess = (event) => {
16 | this.reply(reply, request.result)
17 | }
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/develop/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | foxql-peer | test browser
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import bridge from './src/bridge.js'
2 | import signallingServer from './src/signalling.js'
3 | import database from './src/database.js'
4 | import sha256 from 'crypto-js/sha256.js'
5 | import { nodeId, node, sigStore, dataPool} from './src/utils/index.js'
6 | import { bridgeNode } from './src/bootNodes.js'
7 | import constantEvents from './src/events.js'
8 | import pkg from 'uuid'
9 |
10 | const { v4: uuidv4 } = pkg
11 |
12 | class p2pNetwork extends bridge{
13 | constructor({bridgeServer, maxNodeCount, maxCandidateCallTime, powPoolingtime, iceServers, dappAlias, wssOptions, nodeIdCache = true})
14 | {
15 | super(bridgeServer || bridgeNode, wssOptions || {})
16 | this.wssOptions = wssOptions || {}
17 | this.signallingServers = {}
18 | this.events = {}
19 | this.replyChannels = {}
20 | this.status = 'not-ready'
21 | this.dappAlias = dappAlias
22 | this.nodeId = 'nothing'
23 | this.nodeAddress = null
24 | this.maxNodeCount = maxNodeCount
25 | this.maxCandidateCallTime = maxCandidateCallTime || 900 // 900 = 0.9 second
26 | this.powPoolingtime = powPoolingtime || 1000 // ms
27 | this.constantSignallingServer = null
28 |
29 | this.indexedDb = new database()
30 |
31 | this.nodeMetaData = {
32 | name: null,
33 | description: null
34 | }
35 |
36 | this.iceServers = iceServers || [
37 | {urls:'stun:stun.l.google.com:19302'},
38 | {urls:'stun:stun4.l.google.com:19302'}
39 | ];
40 |
41 | this.sigStore = new sigStore(maxNodeCount)
42 |
43 | this.nodes = {}
44 | this.connectedNodeCount = 0
45 |
46 | this.nodeIdCache = nodeIdCache
47 |
48 | }
49 |
50 | start(databaseListeners)
51 | {
52 |
53 | this.connectBridge(
54 | this.listenSignallingServer,
55 | this.dappAlias
56 | )
57 |
58 | this.loadEvents(constantEvents)
59 |
60 | if(databaseListeners !== undefined){
61 | this.indexedDb.open(databaseListeners)
62 | }
63 |
64 | this.nodeId = nodeId(this.nodeIdCache)
65 |
66 | }
67 |
68 | listenSignallingServer({host}, simulationListener = true)
69 | {
70 | if(host === undefined) return false
71 |
72 | const key = sha256(host).toString()
73 | if(this.existSignallingServer(key)) { // signalling Server is found
74 | return key
75 | }
76 | const signallingServerInstance = new signallingServer(host, ()=> {
77 | if(this.status === 'not-ready') { // if first signalling server connection
78 | this.generateNodeAddress(host)
79 | this.constantSignallingServer = key
80 | }
81 | this.upgradeConnection(key)
82 | this.status = 'ready'
83 | }, simulationListener ? this.eventSimulation.bind(this) : null, this.wssOptions)
84 |
85 | const self = this
86 |
87 | signallingServerInstance.loadEvents(constantEvents, self)
88 |
89 | this.signallingServers[key] = signallingServerInstance
90 |
91 | return key
92 | }
93 |
94 | upgradeConnection(key)
95 | {
96 | this.signallingServers[key].signallingSocket.emit('upgrade', {
97 | nodeId: this.nodeId,
98 | dappAlias: this.dappAlias
99 | })
100 | }
101 |
102 | existSignallingServer(key)
103 | {
104 | return this.signallingServers[key] !== undefined ? true : false
105 | }
106 |
107 | loadEvents(events)
108 | {
109 | events.forEach( ({listener, listenerName}) => {
110 | this.on(listenerName, listener)
111 | });
112 | }
113 |
114 | on(listenerName, listener)
115 | {
116 | this.events[listenerName] = listener.bind(this)
117 | }
118 |
119 | async ask({transportPackage, livingTime = 1000, stickyNode = false, localWork = false})
120 | {
121 | if(this.status !== 'ready') return {warning: this.status}
122 | const tempListenerName = uuidv4()
123 | const {question} = this.sigStore.generate(this.maxCandidateCallTime)
124 |
125 | let powOffersPool = []
126 |
127 | this.temporaryBridgelistener(tempListenerName, (node)=> {powOffersPool.push(node)})
128 |
129 | this.transportMessage({
130 | ...transportPackage,
131 | nodeId: this.nodeId,
132 | temporaryListener: tempListenerName,
133 | livingTime: livingTime,
134 | nodeAddress: this.nodeAddress,
135 | powQuestion: question
136 | })
137 |
138 | await this.sleep(livingTime) // wait pool
139 |
140 | if(this.connectedNodeCount <= 0) return false
141 |
142 | const pool = new dataPool()
143 |
144 | const poollingListenerName = uuidv4()
145 | this.events[poollingListenerName] = (data) => {
146 | pool.listen(data)
147 |
148 | if(!stickyNode){
149 | const node = this.nodes[data.nodeId]
150 | node.close()
151 | }
152 | }
153 |
154 | transportPackage = {
155 | ...transportPackage,
156 | sender: {
157 | listener: poollingListenerName,
158 | nodeId: this.nodeId
159 | }
160 | }
161 |
162 | let effectedNodes = localWork ? powOffersPool : this.nodes
163 |
164 | for(let nodeId in effectedNodes){
165 | const node = effectedNodes[nodeId] || false
166 | node.send(transportPackage)
167 | }
168 |
169 | await this.sleep(this.powPoolingtime)
170 | delete this.events[poollingListenerName]
171 | return pool.export()
172 |
173 | }
174 |
175 | temporaryBridgelistener(tempListenerName, callback)
176 | {
177 | this.bridgeSocket.on(tempListenerName, ({nodeAddress, powQuestionAnswer}) => { // listen transport event result
178 |
179 | if(!this.sigStore.isvalid(powQuestionAnswer)){
180 | return false
181 | }
182 |
183 | if(this.connectedNodeCount >= this.maxNodeCount) return false
184 | this.bridgeSocket.emit('pow-is-correct', powQuestionAnswer)
185 | const {nodeId, signallingServerAddress} = this.parseNodeAddress(nodeAddress)
186 | const signallHash = this.listenSignallingServer({host: signallingServerAddress}, false)
187 | const targetNode = this.createNode(signallHash, nodeId)
188 | targetNode.createOffer(powQuestionAnswer.answer, signallHash)
189 | callback(targetNode)
190 | })
191 | }
192 |
193 | async sleep(ms)
194 | {
195 | return new Promise((resolve)=> {
196 | setTimeout(()=> {
197 | resolve(true)
198 | }, ms)
199 | })
200 | }
201 |
202 | async eventSimulation(eventObject)
203 | {
204 | const {eventPackage, powQuestion} = eventObject
205 | const {nodeId, p2pChannelName} = eventPackage
206 |
207 | if(nodeId === this.nodeId) return false
208 |
209 | if(this.findNode(nodeId)) return false// node currently connected.
210 |
211 | const listener = this.findNodeEvent(p2pChannelName) // find p2p event listener
212 |
213 | if(!listener) return false
214 |
215 | const simulateProcess = await listener(eventPackage, true)
216 |
217 | if(!simulateProcess) return false
218 |
219 | const powQuestionAnswer = this.sigStore.solveQuestion(powQuestion)
220 | if(!powQuestionAnswer) return false
221 |
222 | await this.sendSimulationDoneSignall(powQuestionAnswer, eventObject)
223 | }
224 |
225 | async sendSimulationDoneSignall(powQuestionAnswer, eventObject)
226 | {
227 | const {bridgePoolingListener} = eventObject
228 |
229 | this.bridgeSocket.emit('transport-pooling', {
230 | bridgePoolingListener: bridgePoolingListener,
231 | nodeAddress: this.nodeAddress,
232 | powQuestionAnswer: powQuestionAnswer
233 | })
234 | }
235 |
236 | findNodeEvent(listenerName)
237 | {
238 | return this.events[listenerName] || false
239 | }
240 |
241 | findNode(nodeId)
242 | {
243 | return this.nodes[nodeId] ? true : false
244 | }
245 |
246 | generateNodeAddress(signallingHostAddress)
247 | {
248 | const {protocol, hostname, port = 80} = new URL(signallingHostAddress)
249 | this.nodeAddress = `${this.nodeId}&${protocol.slice(0, -1)}&${hostname}&${port}`
250 | }
251 |
252 | parseNodeAddress(nodeAddress)
253 | {
254 | const [nodeId, protocol, host, port] = nodeAddress.split('&')
255 |
256 | const signallingServerAddress = `${protocol}://${host}:${port}`
257 |
258 | return {
259 | nodeId,
260 | signallingServerAddress
261 | }
262 |
263 | }
264 |
265 | reply({nodeId, listener}, data)
266 | {
267 | const targetNode = this.nodes[nodeId] || false
268 | if(!targetNode) return false
269 |
270 | targetNode.send({
271 | p2pChannelName: listener,
272 | data: data,
273 | nodeId: this.nodeId
274 | })
275 |
276 | return true
277 | }
278 |
279 | createNode(signallHash, nodeId)
280 | {
281 | const candidateNode = new node(
282 | this.iceServers,
283 | this.signallingServers[signallHash],
284 | this
285 | )
286 |
287 | candidateNode.create(nodeId)
288 |
289 | this.nodes[nodeId] = candidateNode
290 | this.connectedNodeCount +=1
291 | return candidateNode
292 | }
293 |
294 | setMetaData({name, description})
295 | {
296 | this.nodeMetaData = {
297 | name: name,
298 | description: description
299 | }
300 | }
301 |
302 | disconnect(id)
303 | {
304 | delete this.nodes[id]
305 | this.connectedNodeCount -= 1
306 | }
307 | }
308 |
309 |
310 | export default p2pNetwork
311 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@foxql/foxql-peer",
3 | "version": "1.4.0",
4 | "description": "Decentralized web apps over web2.",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "parcel develop/index.html"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/foxql/peer.git"
12 | },
13 | "keywords": [
14 | "p2p",
15 | "peer",
16 | "to",
17 | "peer",
18 | "webrtc",
19 | "dapp"
20 | ],
21 | "author": "Bora Erdinç Özer",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/foxql/peer/issues"
25 | },
26 | "homepage": "https://github.com/foxql/peer#readme",
27 | "dependencies": {
28 | "crypto-js": "^4.1.1",
29 | "socket.io-client": "^4.1.2",
30 | "uuid": "^3.4.0"
31 | },
32 | "browserslist": [
33 | "last 1 Chrome versions"
34 | ],
35 | "devDependencies": {
36 | "buffer": "^6.0.3",
37 | "parcel": "^2.6.0"
38 | },
39 | "type": "module"
40 | }
41 |
--------------------------------------------------------------------------------
/src/bootNodes.js:
--------------------------------------------------------------------------------
1 | export const bridgeNode = {
2 | host: 'https://foxql-bridge.herokuapp.com'
3 | }
4 |
--------------------------------------------------------------------------------
/src/bridge.js:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client'
2 | export default class {
3 |
4 | constructor({host}, options)
5 | {
6 | this.host = host
7 | this.options = options
8 | this.bridgeStatus = 'not-ready'
9 | this.bridgeSocket = null
10 | }
11 |
12 | connectBridge(callback, dappAlias)
13 | {
14 | const socket = io(this.host, this.options)
15 | socket.on('connect', ()=> {
16 | this.bridgeStatus = 'connected'
17 | socket.emit('upgrade-dapp', dappAlias)
18 | socket.emit('find-available-server', true)
19 | })
20 | let interval = setInterval(()=> {
21 | if(this.status == 'ready'){
22 | clearInterval(interval)
23 | return
24 | }
25 | socket.emit('find-available-server', true)
26 | }, 700)
27 | socket.on('find-available-server', callback.bind(this))
28 |
29 | this.bridgeSocket = socket
30 | }
31 |
32 | transportMessage(data)
33 | {
34 | this.bridgeSocket.emit('transport', data)
35 | }
36 |
37 |
38 | }
--------------------------------------------------------------------------------
/src/database.js:
--------------------------------------------------------------------------------
1 | const databaseName = 'foxql-app-storage'
2 |
3 | export default class {
4 |
5 | constructor()
6 | {
7 | this.status = 'waiting'
8 | this.request = null
9 | this.ready = false
10 | this.db = null
11 | }
12 |
13 | open({version, onupgradeneeded, onerror})
14 | {
15 | const indexedDb = indexedDB || window.indexedDB ||
16 | window.mozIndexedDB ||
17 | window.webkitIndexedDB ||
18 | window.msIndexedDB ||
19 | window.shimIndexedDB
20 |
21 | if(!indexedDb){
22 | this.status = 'IndexedDB could not be found in this browser.'
23 | return false
24 | }
25 |
26 | this.request = indexedDb.open(databaseName, version || 1)
27 | this.request.onerror = onerror.bind(this)
28 | this.request.onupgradeneeded = onupgradeneeded.bind(this)
29 | this.request.onsuccess = (e)=> {
30 | this.status = 'connected'
31 | this.ready = true
32 | this.db = e.target.result
33 | }
34 | }
35 |
36 | transaction(storeName, operation)
37 | {
38 | return this.db.transaction(storeName, operation)
39 | }
40 | }
--------------------------------------------------------------------------------
/src/events.js:
--------------------------------------------------------------------------------
1 | import * as offer from './events/offer.js'
2 | import * as candidate from './events/candidate.js'
3 | import * as answer from './events/answer.js'
4 | export default [
5 | offer,
6 | candidate,
7 | answer
8 | ]
--------------------------------------------------------------------------------
/src/events/answer.js:
--------------------------------------------------------------------------------
1 | export const listenerName = 'answer'
2 |
3 | export async function listener (data, simulated = false)
4 | {
5 | const {from, sdp} = data
6 |
7 | const targetNode = this.nodes[from] || false
8 |
9 | if(!targetNode) return
10 |
11 | targetNode.p2p.setRemoteDescription({type: "answer", sdp: sdp})
12 | }
--------------------------------------------------------------------------------
/src/events/candidate.js:
--------------------------------------------------------------------------------
1 | export const listenerName = 'candidate';
2 |
3 | export async function listener (data, simulate = false)
4 | {
5 | const {to, candidate, signature} = data
6 |
7 | // todo
8 |
9 | return false
10 | }
--------------------------------------------------------------------------------
/src/events/drop.js:
--------------------------------------------------------------------------------
1 | const name = 'drop';
2 |
3 | async function listener (network, peerId)
4 | {
5 | if(network.connections[peerId] !== undefined) {
6 | network.connections[peerId].dataChannel.close();
7 | delete network.connections[peerId];
8 | }
9 | }
10 |
11 | export default {
12 | name : name,
13 | listener : listener
14 | };
--------------------------------------------------------------------------------
/src/events/offer.js:
--------------------------------------------------------------------------------
1 | export const listenerName = 'offer'
2 |
3 | export async function listener(data, simulate = false)
4 | {
5 | const {signature, from, sdp} = data
6 | const access = this.sigStore.existWhiteList(signature)
7 |
8 | if(!access) return
9 | const offerNode = this.createNode(this.constantSignallingServer, from)
10 | offerNode.createAnswer(signature, sdp)
11 | }
--------------------------------------------------------------------------------
/src/signalling.js:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client'
2 | export default class {
3 |
4 | constructor(host, connectionCallback, eventSimulationListener, options)
5 | {
6 | this.host = host
7 | this.signallingStatus = 'not-ready'
8 | this.signallingSocket = null
9 | this.connectionCallback = connectionCallback
10 | this.eventSimulationListener = eventSimulationListener
11 | this.options = options
12 | this.connectSignallingServer()
13 | }
14 |
15 | connectSignallingServer()
16 | {
17 | const socket = io(this.host, this.options)
18 | socket.on('connect', ()=> {
19 | this.signallingStatus = 'connected'
20 | this.connectionCallback(this.host)
21 | })
22 | socket.on('disconnect', this.disconnectListener)
23 |
24 | if(typeof this.eventSimulationListener === 'function') {
25 | socket.on('eventSimulation', this.eventSimulationListener)
26 | }
27 |
28 |
29 | this.signallingSocket = socket
30 | }
31 |
32 | loadEvents(eventList, parent)
33 | {
34 | eventList.forEach( ({listener, listenerName}) => {
35 | this.signallingSocket.on(listenerName, listener.bind(parent))
36 | });
37 | }
38 |
39 | disconnectListener()
40 | {
41 | this.signallingStatus = 'disconnect'
42 | }
43 |
44 |
45 | }
--------------------------------------------------------------------------------
/src/utils/dataPool.js:
--------------------------------------------------------------------------------
1 | import sha256 from 'crypto-js/sha256.js'
2 |
3 | export default class {
4 |
5 | constructor()
6 | {
7 | this.results = []
8 | this.nodes = {}
9 | this.nodeCount = 0
10 | this.currentResultIndex = 0
11 |
12 | this.hashMap = new Map()
13 | }
14 |
15 | encrypt(data)
16 | {
17 | return sha256(JSON.stringify(data)).toString()
18 | }
19 |
20 | push(data, nodeId)
21 | {
22 | const hash = this.encrypt(data)
23 | const currentIndex = this.currentResultIndex
24 | if(this.hashMap.has(hash)){
25 | let currentWeight = this.hashMap.get(hash)
26 | this.hashMap.set(hash, {
27 | currentIndex: currentIndex - 1,
28 | weight: currentWeight.weight + 1
29 | })
30 | this.nodes[nodeId].push(currentIndex - 1)
31 | return true
32 | }
33 |
34 | this.results.push(data)
35 | this.nodes[nodeId].push(currentIndex)
36 |
37 | this.hashMap.set(hash, {
38 | weight: 0,
39 | resultIndex: currentIndex
40 | })
41 | this.currentResultIndex++
42 | }
43 |
44 | export()
45 | {
46 | return {
47 | results: {
48 | count: this.results.length,
49 | data: this.results,
50 | weight: this.hashMap.entries()
51 | },
52 | senders: {
53 | nodes: this.nodes,
54 | count: this.nodeCount
55 | }
56 | }
57 | }
58 |
59 | listen({data, nodeId})
60 | {
61 | this.createNode(nodeId)
62 |
63 | if(Array.isArray(data)){
64 | data.forEach(item => {
65 | this.push(item, nodeId)
66 | })
67 |
68 | return true
69 | }
70 |
71 | this.push(data, nodeId)
72 | }
73 |
74 | createNode(nodeId)
75 | {
76 | if(this.nodes[nodeId] === undefined) {
77 | this.nodes[nodeId] = []
78 | this.nodeCount++
79 | }
80 | }
81 |
82 |
83 |
84 | }
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import nodeId from './nodeId.js'
2 | import sigStore from './sigStore.js'
3 | import node from './node.js'
4 | import dataPool from './dataPool.js'
5 | export {
6 | nodeId,
7 | sigStore,
8 | node,
9 | dataPool
10 | }
--------------------------------------------------------------------------------
/src/utils/node.js:
--------------------------------------------------------------------------------
1 | const defaultDataChannelName = 'foxql-native-channel'
2 |
3 | class node {
4 |
5 | constructor(iceServers, signallingServer, container)
6 | {
7 | this.iceServers = iceServers
8 | this.socket = signallingServer.signallingSocket
9 | this.id = null
10 | this.p2p = null
11 | this.channel = null
12 | this.waitingMessage = false
13 | this.container = container
14 | }
15 |
16 | async create(id)
17 | {
18 | this.id = id
19 | const p2p = new RTCPeerConnection({
20 | iceServers : this.iceServers,
21 | sdpSemantics: "unified-plan"
22 | })
23 |
24 | this.channel = p2p.createDataChannel(defaultDataChannelName, {negotiated: true, id: 0})
25 |
26 | this.channel.onopen = this.handleDataChannelOpen.bind(this)
27 | this.channel.onmessage = this.handleDataChannelMessage.bind(this)
28 | this.channel.onclose = ()=> {
29 | this.container.disconnect(this.id)
30 | }
31 | p2p.oniceconnectionstatechange = e => {
32 | if(p2p.iceConnectionState === 'failed'){
33 | this.container.disconnect(this.id)
34 | }
35 | }
36 |
37 | this.p2p = p2p
38 | }
39 |
40 | handleDataChannelOpen()
41 | {
42 | console.log('Datachannel is ready', this.id)
43 | }
44 |
45 | async handleDataChannelMessage({data})
46 | {
47 | const parsedPackage = this.parsePackage(data)
48 | if(!parsedPackage) return false
49 |
50 | const {p2pChannelName} = parsedPackage
51 |
52 | const listener = this.container.findNodeEvent(p2pChannelName)
53 |
54 | if(!listener) return false
55 |
56 | await listener(parsedPackage, false)
57 | }
58 |
59 | parsePackage(data)
60 | {
61 | try{
62 | return JSON.parse(data)
63 | }catch(e){
64 | return false
65 | }
66 | }
67 |
68 | send(message)
69 | {
70 | if(this.channel.readyState !== 'open') return false
71 | this.channel.send(JSON.stringify(message))
72 | }
73 |
74 | async createOffer(signature)
75 | {
76 | const offer = await this.p2p.createOffer()
77 | await this.p2p.setLocalDescription(offer)
78 | this.p2p.onicecandidate = ({candidate}) => {
79 | if (candidate) {
80 | this.sendCandidateSignall(signature, candidate)
81 | return;
82 | }
83 | this.socket.emit('offer', {
84 | to : this.id,
85 | offer : this.p2p.localDescription.sdp,
86 | signature: signature
87 | });
88 | }
89 | }
90 |
91 | async sendCandidateSignall(signature, candidate)
92 | {
93 | this.socket.emit('candidate', {
94 | candidate : candidate,
95 | to : this.id,
96 | signature: signature
97 | });
98 | }
99 |
100 | async createAnswer(signature, offerSdp)
101 | {
102 | await this.p2p.setRemoteDescription({type: "offer", sdp: offerSdp});
103 | const answer = await this.p2p.createAnswer()
104 | await this.p2p.setLocalDescription(answer);
105 | this.p2p.onicecandidate = ({candidate}) => {
106 | if (candidate) {
107 | this.sendCandidateSignall(signature, candidate)
108 | return
109 | }
110 | this.socket.emit('answer', {
111 | to : this.id,
112 | answer : this.p2p.localDescription.sdp,
113 | signature: signature
114 | })
115 | }
116 | }
117 |
118 | close()
119 | {
120 | this.p2p.close()
121 | }
122 | }
123 |
124 |
125 | export default node
--------------------------------------------------------------------------------
/src/utils/nodeId.js:
--------------------------------------------------------------------------------
1 | import pkg from 'uuid';
2 | const { v4: uuidv4 } = pkg;
3 | const localstorageKey = 'foxql-node-id'
4 |
5 |
6 |
7 | function find()
8 | {
9 | try {
10 | return localStorage.getItem(localstorageKey) || false
11 | }catch(e){
12 | return false
13 | }
14 |
15 | }
16 |
17 | function set()
18 | {
19 | try {
20 | localStorage.setItem(localstorageKey, uuidv4())
21 | }catch(e){
22 | return uuidv4()
23 | }
24 | }
25 |
26 | export default (cache)=> {
27 | if(!cache) {
28 | return uuidv4()
29 | }
30 |
31 | if(!find()){
32 | return set()
33 | }
34 |
35 | return find()
36 | }
--------------------------------------------------------------------------------
/src/utils/sigStore.js:
--------------------------------------------------------------------------------
1 | import pkg from 'uuid';
2 | const { v4: uuidv4 } = pkg;
3 | import sha256 from 'crypto-js/sha256.js'
4 |
5 | export default class {
6 |
7 | constructor(maxCount)
8 | {
9 | this.maxNonce = 99999
10 | this.minNonce = 1
11 | this.letterCountForClue = 6
12 | this.maxSignatureCount = maxCount
13 | this.signatures = {}
14 |
15 | this.whiteList = {}
16 | this.livingSignatureCount = 0
17 | }
18 |
19 | generate(destroyTime)
20 | {
21 | const uuidKey = uuidv4()
22 | const nonce = this.randomNonce()
23 | const key = sha256(uuidKey + nonce).toString()
24 | this.signatures[key] = nonce
25 | this.livingSignatureCount += 1
26 | setTimeout(()=> {
27 | this.dropSignature(key)
28 | }, destroyTime)
29 | return {
30 | question: {
31 | uuid: uuidKey,
32 | startWith: key.substring(0, this.letterCountForClue)
33 | }
34 | }
35 | }
36 |
37 | dropSignature(key)
38 | {
39 | delete this.signatures[key]
40 | this.livingSignatureCount -= 1
41 | }
42 |
43 | limitControl()
44 | {
45 | return this.livingSignatureCount < this.maxSignatureCount
46 | }
47 |
48 | find(key)
49 | {
50 | return this.signatures[key] || false
51 | }
52 |
53 | randomNonce()
54 | {
55 | return Math.floor(Math.random() * this.maxNonce) + this.minNonce
56 | }
57 |
58 | solveQuestion({uuid, startWith})
59 | {
60 | for(let nonce = this.minNonce; nonce <= this.maxNonce; nonce++){
61 | const key = sha256(uuid + nonce).toString()
62 | if(key.substring(0, this.letterCountForClue) == startWith){
63 | this.whiteList[key] = nonce
64 | this.dropWhiteList(key)
65 | return {
66 | answer: key,
67 | nonce: nonce
68 | }
69 | }
70 | }
71 | return false
72 | }
73 |
74 | isvalid({answer, nonce})
75 | {
76 | const findNonce = this.signatures[answer] || false
77 | if(!findNonce) return false
78 |
79 | return findNonce === nonce
80 | }
81 |
82 | dropWhiteList(key)
83 | {
84 | setTimeout(() => {
85 | this.whiteList[key] = undefined
86 | }, 1000)
87 | }
88 |
89 | existWhiteList(key)
90 | {
91 | return this.whiteList[key] || false
92 | }
93 |
94 |
95 | }
--------------------------------------------------------------------------------