├── .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 | } --------------------------------------------------------------------------------