commit fa167b5757214c00270071339c94d854d9bea439 Author: Charles Iliya Krempeaux Date: Fri Sep 22 18:25:02 2023 +0900 initial commits diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a3bf392 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Charles Iliya Krempeaux :: http://changelog.ca/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cef02f3 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# go-httpbearer + +Package **httpbearer** provides tool to deal with HTTP bearer tokens, for the Go programming language. + +## Online Documention + +Online documentation, which includes examples, can be found at: http://godoc.org/sourcecode.social/reiver/go-httpbearer + +[![GoDoc](https://godoc.org/sourcecode.social/reiver/go-httpbearer?status.svg)](https://godoc.org/sourcecode.social/reiver/go-httpbearer) + +## Example + +Here is an example: + +```go + import "sourcecode.social/reiver/go-httpbearer" + + value := req.Header.Get("Authorization") + + // ... + + bearerToken, successful := httpbearer.Parse(value) + if !successful { + //@TODO: the value of the Authorization header was not a bearer token. + return ErrNotBearerToken + } +``` + +If the Authorization header was: +``` +Authorization: Bearer WW91IGFyZSBub3QgYSBkcm9wIGluIHRoZSBvY2Vhbi4gWW91IGFyZSB0aGUgZW50aXJlIG9jZWFuLCBpbiBhIGRyb3Au +``` + +Then the value of `bearerToken` would be: +```go +"WW91IGFyZSBub3QgYSBkcm9wIGluIHRoZSBvY2Vhbi4gWW91IGFyZSB0aGUgZW50aXJlIG9jZWFuLCBpbiBhIGRyb3Au" +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..39a7b0b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module sourcecode.social/reiver/go-httpbearer + +go 1.20 diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..5d6b648 --- /dev/null +++ b/parse.go @@ -0,0 +1,180 @@ +package httpbearer + +import ( + "strings" +) + +// Parse parses the value of an HTTP "Authorization" header and returns a bearer token, if there was one. +// If there was no bearer token in the HTTP "Authorization" header, then the returned value of ‘successful’ will be false. +// +// The full HTTP request header will look something like this: +// +// "Authorization: Bearer WW91IGFyZSBub3QgYSBkcm9wIGluIHRoZSBvY2Vhbi4gWW91IGFyZSB0aGUgZW50aXJlIG9jZWFuLCBpbiBhIGRyb3Au\r\n" +// +// This function expected to receive just the value. So, with our previous example, that would be: +// +// "Bearer WW91IGFyZSBub3QgYSBkcm9wIGluIHRoZSBvY2Vhbi4gWW91IGFyZSB0aGUgZW50aXJlIG9jZWFuLCBpbiBhIGRyb3Au" +// +// Note that HTTP headers allow for extra "\t" and " " to be put in any place where a "\t" or " " already is. +// +// So, for example, this: +// +// "Authorization: Bearer abcde12345\r\n" +// +// And these: +// +// "Authorization: Bearer abcde12345\r\n" +// +// "Authorization: Bearer abcde12345\r\n" +// +// "Authorization: Bearer abcde12345\r\n" +// +// "Authorization: Bearer abcde12345\r\n" +// +// "Authorization: Bearer abcde12345\r\n" +// +// "Authorization: Bearer\tabcde12345\r\n" +// +// "Authorization: Bearer\t\tabcde12345\r\n" +// +// "Authorization: Bearer\t\t\tabcde12345\r\n" +// +// "Authorization: Bearer\t\t\t\tabcde12345\r\n" +// +// "Authorization: Bearer\t\t\t\t\tabcde12345\r\n" +// +// "Authorization: Bearer\t\t\t\t\t\tabcde12345\r\n" +// +// "Authorization: Bearer \t \t abcde12345\r\n" +// +// Are all equivalent. +// +// Also note that HTTP headers also allow for the values to be broken up in multiple lines. +// The rule is that if a "\r\n" is followed by a " " or "\t" the that next line is part of the previous line. +// +// So, for example, this: +// +// "Authorization: Bearer abcde12345\r\n" +// +// And these: +// +// "Authorization:\r\n Bearer abcde12345\r\n" +// +// "Authorization:\r\n\tBearer abcde12345\r\n" +// +// "Authorization: Bearer\r\n abcde12345\r\n" +// +// "Authorization: Bearer\r\n\tabcde12345\r\n" +// +// "Authorization:\r\n \r\n\t Bearer \r\n\t \t\t\t abcde12345\r\n" +// +// Are all equivalent. +// +// Parse deals with all of these, too. +func Parse(value string) (bearerToken string, successful bool) { + + // Although the first important thing we expected it the string "Bearer", + // there could be zero or more of these characters. + // + // • "\t" i.e,. horizontal tab (␉) + // • " " i.e., space (␠) + // • "\r\n" i.e., carriage return (␍), line feed (␊) + // + // In IETF RFC822 LWSP-char is defined as a horizontal tab (␉) or space (␠). + // + // Technically "\r\n" should always be followed by a " " or a "\t". + // But we don't have to worry about that here. As the parser for the request + // already dealt with that. + value = trimleft(value) + + // The first important thing we should see it "Bearer". + // + //@TODO: should this be case insensitive? + { + const expected string = "Bearer" + + if !strings.HasPrefix(value, expected) { + return "", false + } + + value = value[len(expected):] + } + + // The next thing we should see is one of these 3: + // + // • "\t" i.e,. horizontal tab (␉) + // • " " i.e., space (␠) + // • "\r\n" i.e., carriage return (␍), line feed (␊) + // + // In IETF RFC822 LWSP-char is defined as a horizontal tab (␉) or space (␠). + // + // Technically "\r\n" should always be followed by a " " or a "\t". + // But we don't have to worry about that here. As the parser for the request + // already dealt with that. + // + // Note that what we are doing here is safe, even if we are dealing with the UTF-8 Unicode encoding. + { + if len(value) <= 0 { + return "", false + } + + c0, value := value[0], value[1:] + + switch c0 { + case ' ','\t': + // Nothing here. We got LWSP-char. So we will continue. + case '\r': + if len(value) <= 0 { + return "", false + } + c1, value := value[0], value[1:] + value = value[1:] + if '\n' != c1 { + return "", false + } + + // Nothing else here. We got "\r\n". So we will continue. + default: + return "", false + } + } + + // There could be more of these characters: + // + // • "\t" i.e,. horizontal tab (␉) + // • " " i.e., space (␠) + // • "\r\n" i.e., carriage return (␍), line feed (␊) + // + // We will consume them (and ignore them) if they are there. + value = trimleft(value) + + // What should be left is the bearer token (with possibly some LWSP-chars or "\r\n" after it). + // + // Note that what we are doing here is safe, even if we are dealing with the UTF-8 Unicode encoding. + { + if len(value) <= 0 { + return "", true + } + + for i,c := range value { + switch c { + case ' ','\t': + return value[:i], true + case '\r': + if len(value) < i+2 { + return value, true + } + + next := value[i+1] + if '\n' != next { + return value[:i+1], true + } + + return value[:i], true + } + } + + return value, true + } + +} diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 0000000..d208a8b --- /dev/null +++ b/parse_test.go @@ -0,0 +1,300 @@ +package httpbearer + +import ( + "testing" +) + +func TestParse(t *testing.T) { + + tests := []struct{ + Value string + Expected string + }{ + { + Value: "Bearer ", + Expected: "", + }, + + + + { + Value: "Bearer 0", + Expected: "0", + }, + { + Value: "Bearer 1", + Expected: "1", + }, + { + Value: "Bearer a", + Expected: "a", + }, + { + Value: "Bearer x", + Expected: "x", + }, + { + Value: "Bearer B", + Expected: "B", + }, + { + Value: "Bearer Z", + Expected: "Z", + }, + { + Value: "Bearer ۴", + Expected: "۴", + }, + { + Value: "Bearer ۵", + Expected: "۵", + }, + { + Value: "Bearer 🙂", + Expected: "🙂", + }, + { + Value: "Bearer 😈", + Expected: "😈", + }, + + + + { + Value: "Bearer a", + Expected: "a", + }, + { + Value: "Bearer ab", + Expected: "ab", + }, + { + Value: "Bearer abc", + Expected: "abc", + }, + { + Value: "Bearer abcd", + Expected: "abcd", + }, + { + Value: "Bearer abcde", + Expected: "abcde", + }, + + + + { + Value: "Bearer a ", + Expected: "a", + }, + { + Value: "Bearer ab ", + Expected: "ab", + }, + { + Value: "Bearer abc ", + Expected: "abc", + }, + { + Value: "Bearer abcd ", + Expected: "abcd", + }, + { + Value: "Bearer abcde ", + Expected: "abcde", + }, + + + + { + Value: "Bearer a\t", + Expected: "a", + }, + { + Value: "Bearer ab\t", + Expected: "ab", + }, + { + Value: "Bearer abc\t", + Expected: "abc", + }, + { + Value: "Bearer abcd\t", + Expected: "abcd", + }, + { + Value: "Bearer abcde\t", + Expected: "abcde", + }, + + + + { + Value: "Bearer a\r\n ", + Expected: "a", + }, + { + Value: "Bearer ab\r\n ", + Expected: "ab", + }, + { + Value: "Bearer abc\r\n ", + Expected: "abc", + }, + { + Value: "Bearer abcd\r\n ", + Expected: "abcd", + }, + { + Value: "Bearer abcde\r\n ", + Expected: "abcde", + }, + + + + { + Value: "Bearer a\r\n\t", + Expected: "a", + }, + { + Value: "Bearer ab\r\n\t", + Expected: "ab", + }, + { + Value: "Bearer abc\r\n\t", + Expected: "abc", + }, + { + Value: "Bearer abcd\r\n\t", + Expected: "abcd", + }, + { + Value: "Bearer abcde\r\n\t", + Expected: "abcde", + }, + + + + { + Value: "Bearer XYZ123\r", + Expected: "XYZ123\r", + }, + { + Value: "Bearer XYZ123\r ", + Expected: "XYZ123\r", + }, + { + Value: "Bearer XYZ123\r\t", + Expected: "XYZ123\r", + }, + { + Value: "Bearer XYZ123\r\r\n ", + Expected: "XYZ123\r", + }, + { + Value: "Bearer XYZ123\r\r\n\t", + Expected: "XYZ123\r", + }, + + + + { + Value: "Bearer awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + Expected: "awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + }, + { + Value: "Bearer\tawcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + Expected: "awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + }, + { + Value: "Bearer \tawcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + Expected: "awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + }, + { + Value: "Bearer\t awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + Expected: "awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + }, + { + Value: "Bearer\r\n\tawcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + Expected: "awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + }, + { + Value: "Bearer \r\n\t awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + Expected: "awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + }, + + + + { + Value: "Bearer awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj ", + Expected: "awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + }, + { + Value: "Bearer awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj\t", + Expected: "awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + }, + { + Value: "Bearer awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj\r\n\t ", + Expected: "awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + }, + } + + for testNumber, test := range tests { + + actual, successful := Parse(test.Value) + expected := test.Expected + + if !successful { + t.Errorf("For test #%d, expected to be able to parse HTTP bearer token successfully but actually didn't.", testNumber) + t.Logf("VALUE: %q", test.Value) + continue + } + + if expected != actual { + t.Errorf("For test #%d, the actual bearer token is not what was expected.", testNumber) + t.Logf("EXPECTED: %q", expected) + t.Logf("ACTUAL: %q", actual) + t.Logf("VALUE: %q", test.Value) + continue + } + } +} + +func TestParse_fail(t *testing.T) { + + tests := []struct{ + Value string + }{ + { + Value: "", + }, + { + Value: "bearer awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + }, + { + Value: "Token awcQ.j/YQLCF-yhC0Dah@wCn_NJe3e[VwKv1gzj!!SP3PHtj", + }, + { + Value: "Bearer", + }, + } + + for testNumber, test := range tests { + + actual, successful := Parse(test.Value) + + if successful { + t.Errorf("For test #%d, expected parseing of HTTP bearer token to be unsuccessfully but actually was.", testNumber) + t.Logf("VALUE: %q", test.Value) + continue + } + + if expected := ""; expected != actual { + t.Errorf("For test #%d, expected result to be empty string but actually wasn't.", testNumber) + t.Logf("EXPECTED: %q", expected) + t.Logf("ACTUAL: %q", actual) + t.Logf("VALUE: %q", test.Value) + continue + } + } +} diff --git a/trimleft.go b/trimleft.go new file mode 100644 index 0000000..7a60b34 --- /dev/null +++ b/trimleft.go @@ -0,0 +1,37 @@ +package httpbearer + +// There could be more of these characters: +// +// • "\t" i.e,. horizontal tab (␉) +// • " " i.e., space (␠) +// • "\r\n" i.e., carriage return (␍), line feed (␊) +// +// We will consume them (and ignore them) if they are there. +func trimleft(value string) string { + + for { + if len(value) <= 0 { + return value + } + + c0 := value[0] + + switch c0 { + case ' ','\t': + value = value[1:] + // Nothing else here. We got LWSP-char. So we will continue. + case '\r': + if len(value) < 2 { + return value + } + c1 := value[1] + if '\n' != c1 { + return value + } + value = value[2:] + // Nothing else here. We got "\r\n". So we will continue. + default: + return value + } + } +} diff --git a/trimleft_test.go b/trimleft_test.go new file mode 100644 index 0000000..bf4cdf3 --- /dev/null +++ b/trimleft_test.go @@ -0,0 +1,184 @@ +package httpbearer + +import ( + "testing" +) + +func TestTrimLeft(t *testing.T) { + + tests := []struct{ + Value string + Expected string + }{ + { + Value: "", + Expected: "", + }, + + + + { + Value: " ", + Expected: "", + }, + { + Value: " ", + Expected: "", + }, + { + Value: " ", + Expected: "", + }, + { + Value: " ", + Expected: "", + }, + { + Value: " ", + Expected: "", + }, + + + + { + Value: "\t", + Expected: "", + }, + { + Value: "\t\t", + Expected: "", + }, + { + Value: "\t\t\t", + Expected: "", + }, + { + Value: "\t\t\t\t", + Expected: "", + }, + { + Value: "\t\t\t\t\t", + Expected: "", + }, + + + + { + Value: "\r\n", + Expected: "", + }, + { + Value: "\r\n\r\n", + Expected: "", + }, + { + Value: "\r\n\r\n\r\n", + Expected: "", + }, + { + Value: "\r\n\r\n\r\n\r\n", + Expected: "", + }, + { + Value: "\r\n\r\n\r\n\r\n\r\n", + Expected: "", + }, + + + + { + Value: "\t \r\n \r\n\t \t\r\n \r\n\r\n", + Expected: "", + }, + + + + { + Value: " ToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: " ToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: " ToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: " ToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: " ToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + + + + { + Value: "\tToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: "\t\tToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: "\t\t\tToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: "\t\t\t\tToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: "\t\t\t\t\tToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + + + + { + Value: "\r\nToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: "\r\n\r\nToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: "\r\n\r\n\r\nToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: "\r\n\r\n\r\n\r\nToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + { + Value: "\r\n\r\n\r\n\r\n\r\nToK3n]]*i4HD2@)", + Expected: "ToK3n]]*i4HD2@)", + }, + + + + { + Value: "\t \r\n \r\n\t \t\r\n \r\n\r\nToK3n]]*i4HD2@", + Expected: "ToK3n]]*i4HD2@", + }, + } + + for testNumber, test := range tests { + + actual := trimleft(test.Value) + expected := test.Expected + + if expected != actual { + t.Errorf("For test #%d, the actual left-trimmed value is not what was expected.", testNumber) + t.Logf("EXPECTED: %q", expected) + t.Logf("ACTUAL: %q", actual) + t.Logf("VALUE: %q", test.Value) + continue + } + } +}