diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index d0ce55920..5f7624a24 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -89,4 +89,7 @@ ^\Qpackages/firecamp-har/src/types.ts\E$ ^\Qplatform/firecamp-platform/__tests__/index.spec.ts\E$ ^\Qpnpm-lock.yaml\E$ +^\Qpackages/firecamp-echo-server/src/assets/unicodedemo.html\E$ ignore$ +^\Qpackages/firecamp-echo-server/src/assets/credentials.ts\E$ +ignore$ \ No newline at end of file diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index e17c1598f..95a07d0a1 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -248,6 +248,7 @@ socketio someplugin someuser SSLn +Streamable subrip szh szhsin @@ -280,6 +281,7 @@ TTr Twotone typcn typesjs +unicodedemo unpressurized urlbar usagestop diff --git a/packages/firecamp-echo-server/package.json b/packages/firecamp-echo-server/package.json index 77b1a3f80..b9583192f 100644 --- a/packages/firecamp-echo-server/package.json +++ b/packages/firecamp-echo-server/package.json @@ -27,6 +27,9 @@ "@nestjs/websockets": "9.0.1", "class-transformer": "0.5.1", "class-validator": "0.14.0", + "cookie-parser": "^1.4.6", + "hawk": "^9.0.1", + "moment": "^2.29.4", "raw-body": "^2.5.2", "reflect-metadata": "0.1.13", "rimraf": "3.0.2", @@ -34,13 +37,16 @@ "socket.io-v2": "npm:socket.io@2.4.1", "socket.io-v3": "npm:socket.io@3.1.2", "socket.io-v4": "npm:socket.io@4.5.4", - "ws": "8.8.0" + "ws": "8.8.0", + "zlib": "^1.0.5" }, "devDependencies": { "@nestjs/cli": "9.0.0", "@nestjs/schematics": "9.0.1", "@nestjs/testing": "9.0.1", + "@types/cookie-parser": "^1.4.4", "@types/express": "4.17.13", + "@types/hawk": "^9.0.4", "@types/node": "18.0.3", "@types/supertest": "2.0.12", "@types/ws": "8.5.3", diff --git a/packages/firecamp-echo-server/src/assets/credentials.ts b/packages/firecamp-echo-server/src/assets/credentials.ts new file mode 100644 index 000000000..4e0fd7e14 --- /dev/null +++ b/packages/firecamp-echo-server/src/assets/credentials.ts @@ -0,0 +1,5 @@ +export const echoUsername = 'firecamp'; +export const echoPassword = 'password'; +export const hawkId = 'dh37fgj492je'; +export const hawkKey = 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn'; +export const oathSigningKey = 'D%2BEdQ-gs%24-%25%402Nu7&'; diff --git a/packages/firecamp-echo-server/src/assets/unicodedemo.html b/packages/firecamp-echo-server/src/assets/unicodedemo.html new file mode 100644 index 000000000..d3917136e --- /dev/null +++ b/packages/firecamp-echo-server/src/assets/unicodedemo.html @@ -0,0 +1,226 @@ + + + +

Unicode Demo

+ +

Taken from + http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt

+ +
+
+        UTF-8 encoded sample plain-text file
+        ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+
+        Markus Kuhn [ˈmaʳkʊs kuːn]  — 2002-07-25
+
+
+        The ASCII compatible UTF-8 encoding used in this plain-text file
+        is defined in Unicode, ISO 10646-1, and RFC 2279.
+
+
+        Using Unicode/UTF-8, you can write in emails and source code things such as
+
+        Mathematics and sciences:
+
+          ∮ E⋅da = Q,  n → ∞, ∑ f(i) = ∏ g(i),      ⎧⎡⎛┌─────┐⎞⎤⎫
+                                                    ⎪⎢⎜│a²+b³ ⎟⎥⎪
+          ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β),    ⎪⎢⎜│───── ⎟⎥⎪
+                                                    ⎪⎢⎜⎷ c₈   ⎟⎥⎪
+          ℕ ⊆ ℕ₀ ⊂ ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ,                   ⎨⎢⎜       ⎟⎥⎬
+                                                    ⎪⎢⎜ ∞     ⎟⎥⎪
+          ⊥ < a ≠ b ≡ c ≤ d ≪ ⊤ ⇒ (⟦A⟧ ⇔ ⟪B⟫),      ⎪⎢⎜ ⎲     ⎟⎥⎪
+                                                    ⎪⎢⎜ ⎳aⁱ-bⁱ⎟⎥⎪
+          2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm     ⎩⎣⎝i=1    ⎠⎦⎭
+
+        Linguistics and dictionaries:
+
+          ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn
+          Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ]
+
+        APL:
+
+          ((V⍳V)=⍳⍴V)/V←,V    ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈
+
+        Nicer typography in plain text files:
+
+          ╔══════════════════════════════════════════╗
+          ║                                          ║
+          ║   • ‘single’ and “double” quotes         ║
+          ║                                          ║
+          ║   • Curly apostrophes: “We’ve been here” ║
+          ║                                          ║
+          ║   • Latin-1 apostrophe and accents: '´`  ║
+          ║                                          ║
+          ║   • ‚deutsche‘ „Anführungszeichen“       ║
+          ║                                          ║
+          ║   • †, ‡, ‰, •, 3–4, —, −5/+5, ™, …      ║
+          ║                                          ║
+          ║   • ASCII safety test: 1lI|, 0OD, 8B     ║
+          ║                      ╭─────────╮         ║
+          ║   • the euro symbol: │ 14.95 € │         ║
+          ║                      ╰─────────╯         ║
+          ╚══════════════════════════════════════════╝
+
+        Combining characters:
+
+          STARGΛ̊TE SG-1, a = v̇ = r̈, a⃑ ⊥ b⃑
+
+        Greek (in Polytonic):
+
+          The Greek anthem:
+
+          Σὲ γνωρίζω ἀπὸ τὴν κόψη
+          τοῦ σπαθιοῦ τὴν τρομερή,
+          σὲ γνωρίζω ἀπὸ τὴν ὄψη
+          ποὺ μὲ βία μετράει τὴ γῆ.
+
+          ᾿Απ᾿ τὰ κόκκαλα βγαλμένη
+          τῶν ῾Ελλήνων τὰ ἱερά
+          καὶ σὰν πρῶτα ἀνδρειωμένη
+          χαῖρε, ὦ χαῖρε, ᾿Ελευθεριά!
+
+          From a speech of Demosthenes in the 4th century BC:
+
+          Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι,
+          ὅταν τ᾿ εἰς τὰ πράγματα ἀποβλέψω καὶ ὅταν πρὸς τοὺς
+          λόγους οὓς ἀκούω· τοὺς μὲν γὰρ λόγους περὶ τοῦ
+          τιμωρήσασθαι Φίλιππον ὁρῶ γιγνομένους, τὰ δὲ πράγματ᾿
+          εἰς τοῦτο προήκοντα,  ὥσθ᾿ ὅπως μὴ πεισόμεθ᾿ αὐτοὶ
+          πρότερον κακῶς σκέψασθαι δέον. οὐδέν οὖν ἄλλο μοι δοκοῦσιν
+          οἱ τὰ τοιαῦτα λέγοντες ἢ τὴν ὑπόθεσιν, περὶ ἧς βουλεύεσθαι,
+          οὐχὶ τὴν οὖσαν παριστάντες ὑμῖν ἁμαρτάνειν. ἐγὼ δέ, ὅτι μέν
+          ποτ᾿ ἐξῆν τῇ πόλει καὶ τὰ αὑτῆς ἔχειν ἀσφαλῶς καὶ Φίλιππον
+          τιμωρήσασθαι, καὶ μάλ᾿ ἀκριβῶς οἶδα· ἐπ᾿ ἐμοῦ γάρ, οὐ πάλαι
+          γέγονεν ταῦτ᾿ ἀμφότερα· νῦν μέντοι πέπεισμαι τοῦθ᾿ ἱκανὸν
+          προλαβεῖν ἡμῖν εἶναι τὴν πρώτην, ὅπως τοὺς συμμάχους
+          σώσομεν. ἐὰν γὰρ τοῦτο βεβαίως ὑπάρξῃ, τότε καὶ περὶ τοῦ
+          τίνα τιμωρήσεταί τις καὶ ὃν τρόπον ἐξέσται σκοπεῖν· πρὶν δὲ
+          τὴν ἀρχὴν ὀρθῶς ὑποθέσθαι, μάταιον ἡγοῦμαι περὶ τῆς
+          τελευτῆς ὁντινοῦν ποιεῖσθαι λόγον.
+
+          Δημοσθένους, Γ´ ᾿Ολυνθιακὸς
+
+        Georgian:
+
+          From a Unicode conference invitation:
+
+          გთხოვთ ახლავე გაიაროთ რეგისტრაცია Unicode-ის მეათე საერთაშორისო
+          კონფერენციაზე დასასწრებად, რომელიც გაიმართება 10-12 მარტს,
+          ქ. მაინცში, გერმანიაში. კონფერენცია შეჰკრებს ერთად მსოფლიოს
+          ექსპერტებს ისეთ დარგებში როგორიცაა ინტერნეტი და Unicode-ი,
+          ინტერნაციონალიზაცია და ლოკალიზაცია, Unicode-ის გამოყენება
+          ოპერაციულ სისტემებსა, და გამოყენებით პროგრამებში, შრიფტებში,
+          ტექსტების დამუშავებასა და მრავალენოვან კომპიუტერულ სისტემებში.
+
+        Russian:
+
+          From a Unicode conference invitation:
+
+          Зарегистрируйтесь сейчас на Десятую Международную Конференцию по
+          Unicode, которая состоится 10-12 марта 1997 года в Майнце в Германии.
+          Конференция соберет широкий круг экспертов по  вопросам глобального
+          Интернета и Unicode, локализации и интернационализации, воплощению и
+          применению Unicode в различных операционных системах и программных
+          приложениях, шрифтах, верстке и многоязычных компьютерных системах.
+
+        Thai (UCS Level 2):
+
+          Excerpt from a poetry on The Romance of The Three Kingdoms (a Chinese
+          classic 'San Gua'):
+
+          [----------------------------|------------------------]
+            ๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช  พระปกเกศกองบู๊กู้ขึ้นใหม่
+          สิบสองกษัตริย์ก่อนหน้าแลถัดไป       สององค์ไซร้โง่เขลาเบาปัญญา
+            ทรงนับถือขันทีเป็นที่พึ่ง           บ้านเมืองจึงวิปริตเป็นนักหนา
+          โฮจิ๋นเรียกทัพทั่วหัวเมืองมา         หมายจะฆ่ามดชั่วตัวสำคัญ
+            เหมือนขับไสไล่เสือจากเคหา      รับหมาป่าเข้ามาเลยอาสัญ
+          ฝ่ายอ้องอุ้นยุแยกให้แตกกัน          ใช้สาวนั้นเป็นชนวนชื่นชวนใจ
+            พลันลิฉุยกุยกีกลับก่อเหตุ          ช่างอาเพศจริงหนาฟ้าร้องไห้
+          ต้องรบราฆ่าฟันจนบรรลัย           ฤๅหาใครค้ำชูกู้บรรลังก์ ฯ
+
+          (The above is a two-column text. If combining characters are handled
+          correctly, the lines of the second column should be aligned with the
+          | character above.)
+
+        Ethiopian:
+
+          Proverbs in the Amharic language:
+
+          ሰማይ አይታረስ ንጉሥ አይከሰስ።
+          ብላ ካለኝ እንደአባቴ በቆመጠኝ።
+          ጌጥ ያለቤቱ ቁምጥና ነው።
+          ደሀ በሕልሙ ቅቤ ባይጠጣ ንጣት በገደለው።
+          የአፍ ወለምታ በቅቤ አይታሽም።
+          አይጥ በበላ ዳዋ ተመታ።
+          ሲተረጉሙ ይደረግሙ።
+          ቀስ በቀስ፥ ዕንቁላል በእግሩ ይሄዳል።
+          ድር ቢያብር አንበሳ ያስር።
+          ሰው እንደቤቱ እንጅ እንደ ጉረቤቱ አይተዳደርም።
+          እግዜር የከፈተውን ጉሮሮ ሳይዘጋው አይድርም።
+          የጎረቤት ሌባ፥ ቢያዩት ይስቅ ባያዩት ያጠልቅ።
+          ሥራ ከመፍታት ልጄን ላፋታት።
+          ዓባይ ማደሪያ የለው፥ ግንድ ይዞ ይዞራል።
+          የእስላም አገሩ መካ የአሞራ አገሩ ዋርካ።
+          ተንጋሎ ቢተፉ ተመልሶ ባፉ።
+          ወዳጅህ ማር ቢሆን ጨርስህ አትላሰው።
+          እግርህን በፍራሽህ ልክ ዘርጋ።
+
+        Runes:
+
+          ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ
+
+          (Old English, which transcribed into Latin reads 'He cwaeth that he
+          bude thaem lande northweardum with tha Westsae.' and means 'He said
+          that he lived in the northern land near the Western Sea.')
+
+        Braille:
+
+          ⡌⠁⠧⠑ ⠼⠁⠒  ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌
+
+          ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞
+          ⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎
+          ⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂
+          ⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙
+          ⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑
+          ⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲
+
+          ⡕⠇⠙ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲
+
+          ⡍⠔⠙⠖ ⡊ ⠙⠕⠝⠰⠞ ⠍⠑⠁⠝ ⠞⠕ ⠎⠁⠹ ⠹⠁⠞ ⡊ ⠅⠝⠪⠂ ⠕⠋ ⠍⠹
+          ⠪⠝ ⠅⠝⠪⠇⠫⠛⠑⠂ ⠱⠁⠞ ⠹⠻⠑ ⠊⠎ ⠏⠜⠞⠊⠊⠥⠇⠜⠇⠹ ⠙⠑⠁⠙ ⠁⠃⠳⠞
+          ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ ⡊ ⠍⠊⠣⠞ ⠙⠁⠧⠑ ⠃⠑⠲ ⠔⠊⠇⠔⠫⠂ ⠍⠹⠎⠑⠇⠋⠂ ⠞⠕
+          ⠗⠑⠛⠜⠙ ⠁ ⠊⠕⠋⠋⠔⠤⠝⠁⠊⠇ ⠁⠎ ⠹⠑ ⠙⠑⠁⠙⠑⠌ ⠏⠊⠑⠊⠑ ⠕⠋ ⠊⠗⠕⠝⠍⠕⠝⠛⠻⠹
+          ⠔ ⠹⠑ ⠞⠗⠁⠙⠑⠲ ⡃⠥⠞ ⠹⠑ ⠺⠊⠎⠙⠕⠍ ⠕⠋ ⠳⠗ ⠁⠝⠊⠑⠌⠕⠗⠎
+          ⠊⠎ ⠔ ⠹⠑ ⠎⠊⠍⠊⠇⠑⠆ ⠁⠝⠙ ⠍⠹ ⠥⠝⠙⠁⠇⠇⠪⠫ ⠙⠁⠝⠙⠎
+          ⠩⠁⠇⠇ ⠝⠕⠞ ⠙⠊⠌⠥⠗⠃ ⠊⠞⠂ ⠕⠗ ⠹⠑ ⡊⠳⠝⠞⠗⠹⠰⠎ ⠙⠕⠝⠑ ⠋⠕⠗⠲ ⡹⠳
+          ⠺⠊⠇⠇ ⠹⠻⠑⠋⠕⠗⠑ ⠏⠻⠍⠊⠞ ⠍⠑ ⠞⠕ ⠗⠑⠏⠑⠁⠞⠂ ⠑⠍⠏⠙⠁⠞⠊⠊⠁⠇⠇⠹⠂ ⠹⠁⠞
+          ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲
+
+          (The first couple of paragraphs of "A Christmas Carol" by Dickens)
+
+        Compact font selection example text:
+
+          ABCDEFGHIJKLMNOPQRSTUVWXYZ /0123456789
+          abcdefghijklmnopqrstuvwxyz £©µÀÆÖÞßéöÿ
+          –—‘“”„†•…‰™œŠŸž€ ΑΒΓΔΩαβγδω АБВГДабвгд
+          ∀∂∈ℝ∧∪≡∞ ↑↗↨↻⇣ ┐┼╔╘░►☺♀ fi�⑀₂ἠḂӥẄɐː⍎אԱა
+
+        Greetings in various languages:
+
+          Hello world, Καλημέρα κόσμε, コンニチハ
+
+        Box drawing alignment tests:                                          █
+                                                                              ▉
+          ╔══╦══╗  ┌──┬──┐  ╭──┬──╮  ╭──┬──╮  ┏━━┳━━┓  ┎┒┏┑   ╷  ╻ ┏┯┓ ┌┰┐    ▊ ╱╲╱╲╳╳╳
+          ║┌─╨─┐║  │╔═╧═╗│  │╒═╪═╕│  │╓─╁─╖│  ┃┌─╂─┐┃  ┗╃╄┙  ╶┼╴╺╋╸┠┼┨ ┝╋┥    ▋ ╲╱╲╱╳╳╳
+          ║│╲ ╱│║  │║   ║│  ││ │ ││  │║ ┃ ║│  ┃│ ╿ │┃  ┍╅╆┓   ╵  ╹ ┗┷┛ └┸┘    ▌ ╱╲╱╲╳╳╳
+          ╠╡ ╳ ╞╣  ├╢   ╟┤  ├┼─┼─┼┤  ├╫─╂─╫┤  ┣┿╾┼╼┿┫  ┕┛┖┚     ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳
+          ║│╱ ╲│║  │║   ║│  ││ │ ││  │║ ┃ ║│  ┃│ ╽ │┃  ░░▒▒▓▓██ ┊  ┆ ╎ ╏  ┇ ┋ ▎
+          ║└─╥─┘║  │╚═╤═╝│  │╘═╪═╛│  │╙─╀─╜│  ┃└─╂─┘┃  ░░▒▒▓▓██ ┊  ┆ ╎ ╏  ┇ ┋ ▏
+          ╚══╩══╝  └──┴──┘  ╰──┴──╯  ╰──┴──╯  ┗━━┻━━┛  ▗▄▖▛▀▜   └╌╌┘ ╎ ┗╍╍┛ ┋  ▁▂▃▄▅▆▇█
+                                                       ▝▀▘▙▄▟
+
+        
+ + + diff --git a/packages/firecamp-echo-server/src/main.ts b/packages/firecamp-echo-server/src/main.ts index 42193859a..8166ffe0e 100644 --- a/packages/firecamp-echo-server/src/main.ts +++ b/packages/firecamp-echo-server/src/main.ts @@ -1,15 +1,21 @@ import { NestFactory } from '@nestjs/core'; -import { WsAdapter, } from '@nestjs/platform-ws'; +import { WsAdapter } from '@nestjs/platform-ws'; // import { IoAdapter } from '@nestjs/platform-socket.io'; import { AppModule } from './app.module'; +import * as cookieParser from 'cookie-parser'; async function bootstrap() { const app = await NestFactory.create(AppModule); - app.enableCors(); + app.enableCors({ + origin: '*', + }); app.useWebSocketAdapter(new WsAdapter(app)); // app.useWebSocketAdapter(new IoAdapter(app)); + app.use(cookieParser()); + await app.listen(3000); console.log(`Application is running on: ${await app.getUrl()}`); + app.getUrl().then((url) => console.log(url)); } bootstrap(); diff --git a/packages/firecamp-echo-server/src/rest/rest.controller.ts b/packages/firecamp-echo-server/src/rest/rest.controller.ts index 08dfb4c75..5b4b1a92d 100644 --- a/packages/firecamp-echo-server/src/rest/rest.controller.ts +++ b/packages/firecamp-echo-server/src/rest/rest.controller.ts @@ -1,262 +1,799 @@ import * as rawBody from 'raw-body'; -import { BadRequestException, Body, Controller, Get, Headers, Param, Post, Put, Query, Req, Response, HttpStatus, HttpCode, Patch, Delete } from '@nestjs/common'; +import { + BadRequestException, + Body, + Controller, + Get, + Headers, + Param, + Post, + Put, + Query, + Req, + Response, + HttpStatus, + HttpCode, + Patch, + Delete, + Head, + Ip, + Header, + StreamableFile, + Res, +} from '@nestjs/common'; +import * as crypto from 'crypto'; +import { response } from 'express'; +import * as Hawk from 'hawk'; +import { Credentials } from 'hawk/lib/server'; +import { createReadStream } from 'fs'; +import { join } from 'path'; +import { createDeflate, createGzip } from 'zlib'; +import { Readable } from 'stream'; +import { + echoUsername, + echoPassword, + hawkId, + hawkKey, + oathSigningKey, +} from 'src/assets/credentials'; +import { + buildOauthSignatureBase, + parseAuthHeader, +} from 'src/utilities/restControllerUtilities'; +import { get } from 'http'; +import * as moment from 'moment'; + @Controller('') export class RestController { + /** Methods */ - /** Methods */ + @Get('get') + get(@Req() req, @Query() queryParams, @Headers() headers) { + const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}`; + const method = 'GET'; + return { url, method, args: queryParams, headers }; + } - @Get('get') - get( - @Req() req, - @Query() queryParams, - @Headers() headers - ) { - const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}` - const method = 'GET'; - return { url, method, args: queryParams, headers } - } - - @Post('post') - @HttpCode(HttpStatus.OK) - async post( - @Req() req, - @Body() body, - @Headers('Content-Type') ct, - @Headers() headers - ) { - const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}` - const method = 'POST'; - if (req.readable) { - // body is ignored by NestJS -> get raw body from request - const raw = await rawBody(req); - body = raw.toString().trim(); - } - let data= {}; - let form = {}; - if(ct == 'application/x-www-form-urlencoded') form = body; - else data = body - return { url, method, form, data, headers } - } - - @Put('put') - async put( - @Req() req, - @Body() body, - @Headers('Content-Type') ct, - @Headers() headers - ) { - const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}` - const method = 'PUT'; - if (req.readable) { - // body is ignored by NestJS -> get raw body from request - const raw = await rawBody(req); - body = raw.toString().trim(); - } - let data= {}; - let form = {}; - if(ct == 'application/x-www-form-urlencoded') form = body; - else data = body - return { url, method, form, data, headers } - } - - @Patch('patch') - async patch( - @Req() req, - @Body() body, - @Headers('Content-Type') ct, - @Headers() headers - ) { - const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}` - const method = 'PATCH'; - if (req.readable) { - // body is ignored by NestJS -> get raw body from request - const raw = await rawBody(req); - body = raw.toString().trim(); - } - - let data= {}; - let form = {}; - if(ct == 'application/x-www-form-urlencoded') form = body; - else data = body - return { url, method, form, data, headers } - } - - @Delete('delete') - async delete( - @Req() req, - @Body() body, - @Headers('Content-Type') ct, - @Headers() headers - ) { - const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}` - const method = 'PATCH'; - if (req.readable) { - // body is ignored by NestJS -> get raw body from request - const raw = await rawBody(req); - body = raw.toString().trim(); - } - - let data= {}; - let form = {}; - if(ct == 'application/x-www-form-urlencoded') form = body; - else data = body - return { url, method, form, data, headers } - } - - - /** Headers */ - - @Get('headers') - headers( - @Req() req, - @Headers() headers - ) { - // const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}` - // const method = 'GET'; - return { headers } + @Post('post') + @HttpCode(HttpStatus.OK) + async post( + @Req() req, + @Body() body, + @Headers('Content-Type') ct, + @Headers() headers + ) { + const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}`; + const method = 'POST'; + if (req.readable) { + // body is ignored by NestJS -> get raw body from request + const raw = await rawBody(req); + body = raw.toString().trim(); } + let data = {}; + let form = {}; + if (ct == 'application/x-www-form-urlencoded') form = body; + else data = body; + return { url, method, form, data, headers }; + } - @Get('response-headers') - responseHeaders( - @Req() req, - @Query() queryParams, - // @Headers() headers, - @Response() res - ) { - // console.log(queryParams) - Object.keys(queryParams).map((k, i)=> { - res.header(k, queryParams[k]); + @Put('put') + async put( + @Req() req, + @Body() body, + @Headers('Content-Type') ct, + @Headers() headers + ) { + const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}`; + const method = 'PUT'; + if (req.readable) { + // body is ignored by NestJS -> get raw body from request + const raw = await rawBody(req); + body = raw.toString().trim(); + } + let data = {}; + let form = {}; + if (ct == 'application/x-www-form-urlencoded') form = body; + else data = body; + return { url, method, form, data, headers }; + } + + @Patch('patch') + async patch( + @Req() req, + @Body() body, + @Headers('Content-Type') ct, + @Headers() headers + ) { + const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}`; + const method = 'PATCH'; + if (req.readable) { + // body is ignored by NestJS -> get raw body from request + const raw = await rawBody(req); + body = raw.toString().trim(); + } + + let data = {}; + let form = {}; + if (ct == 'application/x-www-form-urlencoded') form = body; + else data = body; + return { url, method, form, data, headers }; + } + + @Delete('delete') + async delete( + @Req() req, + @Body() body, + @Headers('Content-Type') ct, + @Headers() headers + ) { + const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}`; + const method = 'PATCH'; + if (req.readable) { + // body is ignored by NestJS -> get raw body from request + const raw = await rawBody(req); + body = raw.toString().trim(); + } + + let data = {}; + let form = {}; + if (ct == 'application/x-www-form-urlencoded') form = body; + else data = body; + return { url, method, form, data, headers }; + } + + /** Headers */ + + @Get('headers') + headers(@Req() req, @Headers() headers) { + // const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}` + // const method = 'GET'; + return { headers }; + } + + @Get('response-headers') + responseHeaders( + @Req() req, + @Query() queryParams, + // @Headers() headers, + @Response() res + ) { + Object.keys(queryParams).map((k) => { + res.header(k, queryParams[k]); + }); + return res.json(queryParams); + } + + /** Authentication Methods */ + + // Basic Auth + @Get('basic-auth') + basicAuth(@Query() queryParams, @Headers() headers, @Response() res) { + if (!('authorization' in headers)) { + return res.status(401).send('Unauthorized'); + } + + const split = headers.authorization.split(' '); + + const type = split[0]; + if (type != 'Basic') { + return res.status(401).send('Unauthorized'); + } + const decoded = Buffer.from(split[1], 'base64').toString(); + const decodedSplit = decoded.split(':'); + if (decodedSplit.length != 2) { + return res.status(401).send('Unauthorized'); + } + const username = decodedSplit[0]; + const password = decodedSplit[1]; + + if (username !== echoUsername || password !== echoPassword) { + return res.status(401).send('Unauthorized'); + } + + return { authenticated: true }; + } + + // Digest Auth + @Get('digest-auth') + digestAuth(@Headers() headers, @Response() res) { + const realm = 'Users'; + + if (!('authorization' in headers)) { + const nonce = crypto.randomBytes(16).toString('base64'); + res + .set({ + 'www-authenticate': `Digest realm="${realm}", nonce="${nonce}"`, }) - return res.json(queryParams); + .status(401); + return res.status(401).send('Unauthorized'); + } + + const split = headers.authorization.split(' '); + const type = split[0]; + if (type !== 'Digest') { + return res.status(401).send('Unauthorized'); } - /** Authentication Methods */ + const argsMap = parseAuthHeader(headers.authorization.slice(7)); - // Basic Auth - @Get('basic-auth') - basicAuth() { - return 'yet to implement' - // return { authenticated: true } + if ( + !( + argsMap['username'] && + argsMap['nonce'] && + argsMap['realm'] && + argsMap['response'] + ) + ) { + return res.status(401).send('Unauthorized'); } - // Digest Auth - @Get('digest-auth') - digestAuth() { - return 'yet to implement' - // return { authenticated: true } + if (argsMap['username'] !== echoUsername) { + return res.status(401).send('Unauthorized'); } - // Hawk Auth - @Get('hawk-auth') - hawkAuth() { - return 'yet to implement' - // return { authenticated: true } + const HA1 = crypto + .createHash('md5') + .update(`${argsMap['username']}:${argsMap['realm']}:${echoPassword}`) + .digest('hex'); + const HA2 = crypto + .createHash('md5') + .update(`GET:/digest-auth`) + .digest('hex'); + const responseCheck = crypto + .createHash('md5') + .update(`${HA1}:${argsMap['nonce']}:${HA2}`) + .digest('hex'); + + if (responseCheck !== argsMap['response']) { + return res.status(401).send('Unauthorized'); } - // OAuth 1.0 - @Get('oauth1') - OAuth1() { - return 'yet to implement' - // return { authenticated: true } + return res.json({ authorized: true }); + } + + // Hawk Auth + @Get('hawk-auth') + async hawkAuth(@Req() req, @Response() res) { + const credentialsFunc = function (id) { + if (id !== hawkId) { + return undefined; + } + + const credentials: Credentials = { + key: hawkKey, + algorithm: 'sha256', + user: 'Steve', + }; + + return credentials; + }; + + let status, message; + + try { + const { credentials, artifacts } = await Hawk.server.authenticate( + req, + credentialsFunc + ); + message = { message: 'Hawk Authentication Successful' }; + + status = 200; + const header = Hawk.server.header(credentials, artifacts); + + res.set({ 'Server-Authorization': header }); + } catch (error) { + message = error.output.payload; + status = 401; + const header = error.output.headers; + res.set(header); } - /** Cookies Manipulation*/ + return res.status(status).json(message); + } - // set cookie - @Get('cookies/set') - cookieSet() { - return 'yet to implement' + // OAuth 1.0 + @Get('oauth1') + OAuth1(@Req() req, @Headers() headers, @Response() res) { + if (!('authorization' in headers)) { + return res.status(401).send('Unauthorized'); } - // get cookie - @Get('cookies') - cookieGet() { - return 'yet to implement' + const authorizationHeader = headers.authorization; + + if (authorizationHeader.split(' ')[0] !== 'OAuth') { + return res.status(401).send('Unauthorized'); } - // delete cookie - @Get('cookies/delete') - cookieDelete() { - return 'yet to implement' + const oauthParams = parseAuthHeader(authorizationHeader.slice(6)); + + const consumerKey = oauthParams['oauth_consumer_key']; + const timestamp = oauthParams['oauth_timestamp']; + const nonce = oauthParams['oauth_nonce']; + const signature = decodeURIComponent(oauthParams['oauth_signature']); + const signatureMethod = oauthParams['oauth_signature_method']; + + const base_uri = `${req.protocol}://${req.get('Host')}${req.originalUrl}`; + const signatureBase = buildOauthSignatureBase('GET', base_uri, oauthParams); + + const expectedSignature = crypto + .createHmac('sha1', oathSigningKey) + .update(signatureBase) + .digest('base64'); + + if (signature !== expectedSignature) { + const status = 'fail'; + const message = 'HMAC-SHA1 verification failed'; + const normalized_param_string = + consumerKey + '&' + nonce + '&' + signatureMethod + '&' + timestamp; + const baseString = signatureBase; + return res.status(401).json({ + status, + message, + base_uri, + normalized_param_string, + base_string: baseString, + signing_key: oathSigningKey, + }); } + const status = 'pass'; + const message = 'OAuth-1.0a signature verification was successful'; - /** Utilities */ + return res.json({ status, message }); + } - // Response Status Code - @Get('status/:status') - status( - @Param('status') status, - @Response() res - ) { - try { - return res.status(status).json({ status }) - } catch (e) { - throw new BadRequestException(e); - } - } - - // Streamed Response - @Get('stream/:chunk') - stream( - @Param('chunk') chunk, - @Response() res - ) { - return 'yet to implement' + /** Cookies Manipulation*/ + + // set cookie + @Get('cookies/set') + cookieSet(@Query() queryParams, @Response() res) { + const payload = { cookies: {} }; + + Object.entries(queryParams).forEach((param) => { + const [key, value] = param; + payload.cookies[key] = value; + res.cookie(key, value); + }); + return res.json(payload); + } + + // get cookie + @Get('cookies') + cookieGet(@Req() req, @Response() res) { + const responseData = { cookies: req.cookies }; + return res.json(responseData); + } + + // delete cookie + @Get('cookies/delete') + cookieDelete(@Req() req, @Query() queryParameters, @Response() res) { + const cookies = { ...req.cookies }; + + Object.keys(queryParameters).forEach((key) => { + res.clearCookie(key); + delete cookies[key]; + }); + + return res.json(cookies); + } + + /** Utilities */ + + // Response Status Code + @Get('status/:status') + status(@Param('status') status, @Response() res) { + try { + return res.status(status).json({ status }); + } catch (e) { + throw new BadRequestException(e); } + } - // Delayed Response - @Get('delay/:seconds') - delay( - @Param('seconds') seconds, - @Response() res - ) { + // Streamed Response + @Get('stream/:chunk') + stream(@Req() req, @Param('chunk') chunk, @Headers() headers) { + const n = parseInt(chunk); + + if (isNaN(n)) { + return; + } + + let responseJson = ''; + for (let i = 0; i < n; i++) { + const args = { n: chunk }; + const url = `${req.protocol}://${req.get('Host')}${req.originalUrl}`; + const responseData = { args, headers, url }; + responseJson += JSON.stringify(responseData, null, 2); + } + + const responseBuffer = Buffer.from(responseJson); + const stream = new Readable(); + stream.push(responseBuffer); + stream.push(null); + + return new StreamableFile(stream); + } - seconds = +seconds; - if (typeof seconds != 'number') throw new BadRequestException('Seconds not valid') - setTimeout(() => { - return res.json({ delay: seconds, in: 'seconds' }) - }, seconds * 1000); + // Delayed Response + @Get('delay/:seconds') + delay(@Param('seconds') seconds, @Response() res) { + seconds = +seconds; + if (typeof seconds != 'number') + throw new BadRequestException('Seconds not valid'); + setTimeout(() => { + return res.json({ delay: seconds, in: 'seconds' }); + }, seconds * 1000); + } + + // Get UTF8 Encoded Response + @Get('encoding/utf8') + @Header('content-type', 'text/html; charset=utf-8') + encoding() { + const file = createReadStream( + join(process.cwd(), 'src/assets/unicodedemo.html') + ); + return new StreamableFile(file); + } + + // GZip Compressed Response + @Get('gzip') + gzip(@Req() req, @Response() res, @Headers() headers) { + const responseData = { gzip: true, headers, method: req.method }; + + const compressionAlgorithm = 'gzip'; + + const acceptedEncodings = headers['accept-encoding']; + + if (acceptedEncodings && acceptedEncodings.includes(compressionAlgorithm)) { + const jsonResponse = JSON.stringify(responseData); + + res.setHeader('Content-Encoding', compressionAlgorithm); + res.setHeader('Content-Type', 'application/json'); + + const compressionStream = createGzip(); + + compressionStream.pipe(res); + compressionStream.write(jsonResponse); + compressionStream.end(); + } else { + res.json({ ...responseData, gzip: false }); } + } - // Get UTF8 Encoded Response - @Get('encoding/utf8') - encoding() { - return 'yet to implement' + // Deflate Compressed Response + @Get('deflate') + deflate(@Req() req, @Response() res, @Headers() headers) { + const responseData = { deflate: true, headers, method: req.method }; + + const compressionAlgorithm = 'deflate'; + + const acceptedEncodings = headers['accept-encoding']; + + if (acceptedEncodings && acceptedEncodings.includes(compressionAlgorithm)) { + const jsonResponse = JSON.stringify(responseData); + + res.setHeader('Content-Encoding', compressionAlgorithm); + res.setHeader('Content-Type', 'application/json'); + + const compressionStream = createDeflate(); + + compressionStream.pipe(res); + compressionStream.write(jsonResponse); + compressionStream.end(); + } else { + res.json({ ...responseData, deflate: false }); } + } + + //IP address in JSON format + @Get('ip') + ip(@Ip() ip) { + return { ip: ip }; + } + + // Date and Time + // Current UTC time + @Get('time/now') + timeNow() { + return new Date().toUTCString(); + } - // GZip Compressed Response - @Get('gzip') - gzip() { - return 'yet to implement' + // Timestamp validity + @Get('time/valid') + timeValid(@Query() queryParams, @Response() res) { + const { timestamp, locale, format } = queryParams; + const strict = queryParams['strict'] + ? queryParams['strict'].toLowerCase() === 'true' + : false; + + const parsed = moment(timestamp, format, locale, strict); + + return res.json({ valid: parsed.isValid() }); + } + + // Format timestamp + @Get('time/format') + timeFormat(@Query() queryParams, @Response() res) { + const { timestamp, locale, format } = queryParams; + if (!timestamp) { + return res + .status(400) + .send('Invalid undefined in `timestamp` query param'); } - // Deflate Compressed Response - @Get('deflate') - deflate() { - return 'yet to implement' + const strict = queryParams['strict'] + ? queryParams['strict'].toLowerCase() === 'true' + : false; + + const parsed = moment(timestamp, format, locale, strict).format(format); + + console.log(parsed); + return res.json({ format: parsed }); + } + + // Extract timestamp unit + @Get('time/extract') + timeExtract(@Query() queryParams, @Response() res) { + const { locale, format } = queryParams; + const timestamp = queryParams.timestamp || new Date().toUTCString(); + const unit = queryParams.unit && queryParams.unit.toLowerCase() || 'year' + const strict = queryParams['strict'] + ? queryParams['strict'].toLowerCase() === 'true' + : false; + + const parsed = moment(timestamp, locale, format, strict) + const obj = { + years: parsed.year(), + months: parsed.month(), + date: parsed.date(), + hours: parsed.hour(), + minutes: parsed.minute(), + seconds: parsed.second(), + milliseconds: parsed.millisecond(), + }; + + return res.json({unit: obj[unit]}) + + } + + // Time addition + @Get('time/add') + timeAddition(@Query() queryParams, @Response() res) { + const { timestamp, locale, format } = queryParams; + if (!timestamp) { + return res + .status(400) + .send('Invalid undefined in `timestamp` query param'); } - //IP address in JSON format - @Get('ip') - ip() { - return 'yet to implement' + const strict = queryParams['strict'] + ? queryParams['strict'].toLowerCase() === 'true' + : false; + + const { + years, + days, + months, + quarters, + weeks, + hours, + minutes, + seconds, + milliseconds, + } = queryParams; + + const additions = [ + { unit: 'year', amount: years }, + { unit: 'day', amount: days }, + { unit: 'month', amount: months }, + { unit: 'quarter', amount: quarters }, + { unit: 'week', amount: weeks }, + { unit: 'hour', amount: hours }, + { unit: 'minute', amount: minutes }, + { unit: 'second', amount: seconds }, + { unit: 'millisecond', amount: milliseconds }, + ]; + + const parsed = moment(timestamp, format, locale, strict); + + additions.forEach((addition) => { + if (!addition.amount) { + return; + } + parsed; + parsed.add(addition.amount, addition.unit); + }); + + console.log(parsed); + return res.json({ sum: parsed }); + } + // Time subtraction + @Get('time/subtract') + timeSubtraction(@Query() queryParams, @Response() res) { + const { timestamp, locale, format } = queryParams; + + if (!timestamp) { + return res + .status(400) + .send('Invalid undefined in `timestamp` query param'); + } + + const strict = queryParams['strict'] + ? queryParams['strict'].toLowerCase() === 'true' + : false; + + const { + years, + days, + months, + quarters, + weeks, + hours, + minutes, + seconds, + milliseconds, + } = queryParams; + + const subtractions = [ + { unit: 'years', amount: years }, + { unit: 'days', amount: days }, + { unit: 'months', amount: months }, + { unit: 'quarters', amount: quarters }, + { unit: 'weeks', amount: weeks }, + { unit: 'hours', amount: hours }, + { unit: 'minutes', amount: minutes }, + { unit: 'seconds', amount: seconds }, + { unit: 'milliseconds', amount: milliseconds }, + ]; + + const parsed = moment(timestamp, format, locale, strict); + + subtractions.forEach((subtraction) => { + if (!subtraction.amount) { + return; + } + parsed.subtract(subtraction.amount, subtraction.unit); + }); + + return res.json({ difference: parsed }); + } + + // Start of time + @Get('time/start') + startOf(@Query() queryParams, @Response() res) { + const { timestamp, locale, format, unit } = queryParams; + + if (!timestamp) { + return res + .status(400) + .send('Invalid undefined in `timestamp` query param'); + } + + const strict = queryParams['strict'] + ? queryParams['strict'].toLowerCase() === 'true' + : false; + + const parsed = moment(timestamp, format, locale, strict); + + parsed.startOf(unit).format(); + + return res.json({ start: parsed.startOf(unit).format() }); + } + + // Object representation + @Get('time/object') + objectRepresentation(@Query() queryParams, @Response() res) { + const { timestamp, locale, format } = queryParams; + + if (!timestamp) { + return res + .status(400) + .send('Invalid undefined in `timestamp` query param'); + } + + const strict = queryParams['strict'] + ? queryParams['strict'].toLowerCase() === 'true' + : false; + const parsed = moment(timestamp, format, locale, strict); + const obj = { + years: parsed.year(), + months: parsed.month(), + date: parsed.date(), + hours: parsed.hour(), + minutes: parsed.minute(), + seconds: parsed.second(), + milliseconds: parsed.millisecond(), + }; + + console.log(obj); + + return res.json(obj); + } + // Before comparisons + @Get('time/before') + timeBefore(@Query() queryParams, @Response() res) { + const { timestamp, locale, format, target } = queryParams; + if (!timestamp) { + return res + .status(400) + .send('Invalid undefined in `timestamp` query param'); + } + if (!target) { + return res.status(400).send('Invalid undefined in `target` query param'); + } + const strict = queryParams['strict'] + ? queryParams['strict'].toLowerCase() === 'true' + : false; + const parsed = moment(timestamp, format, locale, strict); + const parsedTarget = moment(target, format, locale, strict); + + return res.json({ before: parsed.isBefore(parsedTarget) }); + } + // After comparisons + @Get('time/after') + timeAfter(@Query() queryParams, @Response() res) { + const { timestamp, locale, format, target } = queryParams; + if (!timestamp) { + return res + .status(400) + .send('Invalid undefined in `timestamp` query param'); + } + if (!target) { + return res.status(400).send('Invalid undefined in `target` query param'); + } + const strict = queryParams['strict'] + ? queryParams['strict'].toLowerCase() === 'true' + : false; + const parsed = moment(timestamp, format, locale, strict); + const parsedTarget = moment(target, format, locale, strict); + + return res.json({ after: parsed.isAfter(parsedTarget) }); + } + + // Between timestamps + @Get('time/between') + timeBetween(@Query() queryParams, @Response() res) { + const { timestamp, locale, format, start, end } = queryParams; + if (!timestamp) { + return res + .status(400) + .send('Invalid undefined in `timestamp` query param'); + } + if (!start) { + return res.status(400).send('Invalid undefined in `start` query param'); + } + if (!end) { + return res.status(400).send('Invalid undefined in `end` query param'); + } + const strict = queryParams['strict'] + ? queryParams['strict'].toLowerCase() === 'true' + : false; + const parsed = moment(timestamp, format, locale, strict); + const parsedStart = moment(start, format, locale, strict); + const parsedEnd = moment(end, format, locale, strict); + + return res.json({ + between: parsed.isAfter(parsedStart) && parsed.isBefore(parsedEnd), + }); + } + + // Leap year check + @Get('time/leap') + leap(@Query() queryParams, @Response() res) { + const { timestamp, locale, format } = queryParams; + if (!timestamp) { + return res + .status(400) + .send('Invalid undefined in `timestamp` query param'); } - /** Utilities / Date and Time */ - // 1. Current UTC time - // 2. Timestamp validity - // 3. Format timestamp - // 4. Extract timestamp unit - // 5. Time addition - // 6. Time subtraction - // 7. Start of time - // 8. Object representation - // 9. Before comparisons - // 10. After comparisons - // 11. Between timestamps - // 12. Leap year check + const strict = queryParams['strict'] + ? queryParams['strict'].toLowerCase() === 'true' + : false; + const parsed = moment(timestamp, format, locale, strict); - /** Auth: Digest */ - // 1. DigestAuth Request + return res.json({ leap: parsed.isLeapYear() }); + } + /** Auth: Digest */ + // 1. DigestAuth Request } diff --git a/packages/firecamp-echo-server/src/utilities/restControllerUtilities.ts b/packages/firecamp-echo-server/src/utilities/restControllerUtilities.ts new file mode 100644 index 000000000..2c2d57c39 --- /dev/null +++ b/packages/firecamp-echo-server/src/utilities/restControllerUtilities.ts @@ -0,0 +1,34 @@ +export const parseAuthHeader = (authorizationHeader) => { + const oauthParams = {}; + + const oauthRegex = /(\w+)="([^"]+)"/g; + + let match; + while ((match = oauthRegex.exec(authorizationHeader)) !== null) { + oauthParams[match[1]] = match[2]; + } + + return oauthParams; +}; + +export const buildOauthSignatureBase = (httpMethod, baseUrl, oauthParameters) => { + // Sort the OAuth parameters alphabetically by name + const sortedParameters = Object.keys(oauthParameters) + .sort() + .filter((key) => key !== 'oauth_signature') + .map((key) => key + '=' + encodeURIComponent(oauthParameters[key])); + + // Create the parameter string by joining the sorted parameters with "&" + const parameterString = sortedParameters.join('&'); + + // Encode the HTTP method and base URL + const encodedHttpMethod = encodeURIComponent(httpMethod); + const encodedBaseUrl = encodeURIComponent(baseUrl); + + // Construct the signature base string + const signatureBase = `${encodedHttpMethod}&${encodedBaseUrl}&${encodeURIComponent( + parameterString + )}`; + + return signatureBase; +} \ No newline at end of file