├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── README.md ├── TODO.md ├── cmd └── orbit │ ├── gen.go │ ├── main.go │ └── version.go ├── examples ├── full │ ├── api │ │ ├── api.orbit │ │ ├── api_msgp_gen.go │ │ ├── api_msgp_gen_test.go │ │ └── api_orbit_gen.go │ ├── client │ │ └── main.go │ └── server │ │ └── main.go └── simple │ ├── client │ └── main.go │ ├── hello │ ├── hello.orbit │ ├── hello_msgp_gen.go │ ├── hello_msgp_gen_test.go │ └── hello_orbit_gen.go │ └── service │ └── main.go ├── go.mod ├── go.sum ├── grml.yaml ├── images ├── logo.png └── logo_small.png ├── internal ├── api │ ├── api.go │ ├── api_gen.go │ ├── api_gen_test.go │ └── api_test.go ├── bytes │ ├── bytes.go │ └── bytes_test.go ├── codegen │ ├── ast │ │ ├── ast.go │ │ ├── datatype.go │ │ └── error.go │ ├── gen │ │ ├── cache.go │ │ ├── gen.go │ │ ├── gen_enum.go │ │ ├── gen_error.go │ │ ├── gen_service.go │ │ ├── gen_service_call.go │ │ ├── gen_service_stream.go │ │ ├── gen_type.go │ │ ├── utils.go │ │ └── utils_test.go │ ├── lexer │ │ ├── lexer.go │ │ ├── lexer_test.go │ │ ├── states.go │ │ └── token.go │ ├── parser │ │ ├── parser.go │ │ ├── parser_datatype.go │ │ ├── parser_run.go │ │ ├── parser_service.go │ │ ├── parser_test.go │ │ └── testdata │ │ │ └── valid.orbit │ ├── validate │ │ ├── validate.go │ │ ├── validate_service.go │ │ └── validate_test.go │ └── version.go ├── rpc │ └── rpc.go ├── strutil │ ├── strutil.go │ └── strutil_test.go └── throttler │ └── throttler.go └── pkg ├── client ├── chain.go ├── client.go ├── client_handler.go ├── client_session.go ├── context.go ├── errors.go ├── hook.go ├── options.go ├── session.go ├── session_rpc read.go ├── session_rpc.go ├── session_rpc_cancel.go ├── session_stream.go └── typedstream.go ├── codec ├── codec.go ├── codec_tester.go ├── json │ ├── json.go │ └── json_test.go └── msgpack │ ├── msgpack.go │ └── msgpack_test.go ├── hook └── log │ ├── client.go │ └── service.go ├── packet ├── packet.go └── packet_test.go ├── service ├── context.go ├── errors.go ├── hook.go ├── options.go ├── service.go ├── service_conn.go ├── service_handler.go ├── session.go ├── session_rpc.go ├── session_rpc_async.go ├── session_rpc_cancel.go ├── session_stream.go └── typedstream.go └── transport ├── quic ├── listener.go ├── options.go ├── session.go ├── stream.go └── transport.go ├── transport.go └── yamux ├── listener.go ├── options.go ├── session.go ├── stream.go └── transport.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .idea 3 | .vscode 4 | /orbit 5 | /bin 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.18.x 5 | - 1.19.x 6 | 7 | before_install: 8 | - go get -t -v ./... 9 | 10 | script: 11 | - go test -race -coverprofile=coverage.txt -covermode=atomic ./pkg/... ./internal/... 12 | 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Roland Singer 2 | Sebastian Borchers -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Roland Singer 4 | Copyright (c) 2018 Sebastian Borchers 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | - write tests 3 | - complete full example 4 | - finish documenting 5 | - Include go report in readme (and fix issues that it reports beforehand) 6 | - add orbit fmt cmd for .orbit files 7 | - disconnect session after TTL? 8 | - use tls.dialContext in yamux, once the new go version has come out -------------------------------------------------------------------------------- /cmd/orbit/gen.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package main 29 | 30 | import ( 31 | "github.com/desertbit/grumble" 32 | "github.com/desertbit/orbit/internal/codegen/gen" 33 | ) 34 | 35 | const ( 36 | argOrbitFiles = "orbit-files" 37 | 38 | flagForce = "force" 39 | ) 40 | 41 | var cmdGen = &grumble.Command{ 42 | Name: "gen", 43 | Help: "generate go code from .orbit file. Args: ", 44 | Run: runGen, 45 | Flags: func(f *grumble.Flags) { 46 | f.Bool("f", flagForce, false, "generate all files, ignoring their last modification time") 47 | }, 48 | Args: func(a *grumble.Args) { 49 | a.StringList(argOrbitFiles, "the paths to the orbit files", grumble.Min(1)) 50 | }, 51 | } 52 | 53 | func init() { 54 | App.AddCommand(cmdGen) 55 | } 56 | 57 | func runGen(ctx *grumble.Context) (err error) { 58 | // Iterate over each provided file path and generate the .orbit file. 59 | for _, fp := range ctx.Args.StringList(argOrbitFiles) { 60 | err = gen.Generate(fp, ctx.Flags.Bool(flagForce)) 61 | if err != nil { 62 | return 63 | } 64 | } 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /cmd/orbit/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package main 29 | 30 | import ( 31 | "github.com/desertbit/grumble" 32 | ) 33 | 34 | // Create the grumble app. 35 | var App = grumble.New(&grumble.Config{ 36 | Name: "orbit", 37 | Description: "orbit's helper application", 38 | }) 39 | 40 | func main() { 41 | grumble.Main(App) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/orbit/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package main 29 | 30 | import ( 31 | "fmt" 32 | 33 | "github.com/desertbit/grumble" 34 | "github.com/desertbit/orbit/internal/codegen" 35 | ) 36 | 37 | var cmdVersion = &grumble.Command{ 38 | Name: "version", 39 | Help: "print the version and exit", 40 | Run: runVersion, 41 | } 42 | 43 | func init() { 44 | App.AddCommand(cmdVersion) 45 | } 46 | 47 | func runVersion(ctx *grumble.Context) (err error) { 48 | fmt.Printf("version: %d.%d\n", codegen.OrbitFileVersion, codegen.CacheVersion) 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /examples/full/api/api.orbit: -------------------------------------------------------------------------------- 1 | /** 2 | An example for a small service that deals with users etc. 3 | It is nowhere near complete enough to represent a real application, 4 | but it should give enough ideas how it could look like using orbit. 5 | */ 6 | 7 | version 1 8 | 9 | errors { 10 | authFailed = 1 11 | notFound = 2 12 | nameAlreadyExists = 3 13 | emailAlreadyExists = 4 14 | } 15 | 16 | service { 17 | call register { 18 | arg: { 19 | email string `validate:"email"` // Automatic email validation by go-playground/validator 20 | password string `validate:"required,min=8"` 21 | } 22 | errors: emailAlreadyExists 23 | } 24 | 25 | call login { 26 | arg: { 27 | user string `validate:"required"` 28 | password string `validate:"required"` 29 | } 30 | errors: authFailed 31 | } 32 | 33 | // No parameters needed. 34 | call logout {} 35 | 36 | call getUsers { 37 | arg: { 38 | afterUserID string 39 | count int `validate:"min=1,max=100"` 40 | } 41 | ret: { users []UserOverview } 42 | } 43 | 44 | call getUser { 45 | arg: { userID string `validate:"required"` } 46 | ret: UserDetail 47 | errors: notFound 48 | } 49 | 50 | call getUserProfileImage { 51 | async // Prevents head-of-line blocking, since a separate stream is opened for each call. 52 | arg: { userID string `validate:"required"` } 53 | ret: { jpeg []byte } 54 | errors: notFound 55 | } 56 | 57 | call createUser { 58 | arg: { 59 | userName string `validate:"required,min=4"` 60 | firstName string `validate:"required"` 61 | lastName string `validate:"required"` 62 | email string `validate:"email"` 63 | } 64 | ret: UserDetail 65 | errors: nameAlreadyExists 66 | } 67 | 68 | call updateUser { 69 | arg: { 70 | userID string `validate:"required"` 71 | userName string `validate:"required,min=4"` 72 | firstName string `validate:"required"` 73 | lastName string `validate:"required"` 74 | status string 75 | email string `validate:"email"` 76 | } 77 | errors: nameAlreadyExists, notFound 78 | } 79 | 80 | call updateUserProfileImage { 81 | async 82 | arg: { 83 | userID string `validate:"required"` 84 | jpeg []byte 85 | } 86 | maxArgSize: 5MB 87 | timeout: 60s 88 | errors: notFound 89 | } 90 | 91 | // A stream can be left open to asynchronously push new data from the server 92 | // to the client. 93 | stream observeNotifications { 94 | arg: { userID string `validate:"required"` } 95 | ret: Notification 96 | errors: notFound 97 | } 98 | } 99 | 100 | type UserOverview { 101 | id string 102 | userName string 103 | firstName string 104 | lastName string 105 | joinedOn time 106 | status string 107 | numberFollowers int 108 | } 109 | 110 | type UserDetail { 111 | overview UserOverview 112 | numberPosts int 113 | numberFollowing int 114 | numberFriends int 115 | userStatus UserStatus 116 | } 117 | 118 | type Notification { 119 | id string 120 | title string 121 | description string 122 | thumbnailJpeg []byte 123 | link string 124 | } 125 | 126 | enum UserStatus { 127 | EmailNotVerified = 1 128 | Active = 2 129 | Blocked = 3 130 | } -------------------------------------------------------------------------------- /examples/full/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() {} 4 | -------------------------------------------------------------------------------- /examples/full/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() {} 4 | -------------------------------------------------------------------------------- /examples/simple/client/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package main 29 | 30 | import ( 31 | "context" 32 | "crypto/tls" 33 | "errors" 34 | "fmt" 35 | "log" 36 | "sync" 37 | "time" 38 | 39 | "github.com/desertbit/orbit/examples/simple/hello" 40 | "github.com/desertbit/orbit/pkg/client" 41 | olog "github.com/desertbit/orbit/pkg/hook/log" 42 | "github.com/desertbit/orbit/pkg/transport/quic" 43 | ) 44 | 45 | func main() { 46 | tr, err := quic.NewTransport(&quic.Options{ 47 | TLSConfig: &tls.Config{ 48 | InsecureSkipVerify: true, 49 | NextProtos: []string{"orbit-simple-example"}, 50 | }, 51 | }) 52 | if err != nil { 53 | log.Fatalln(err) 54 | } 55 | 56 | c, err := hello.NewClient(&client.Options{ 57 | Host: "127.0.0.1:1122", 58 | Transport: tr, 59 | Hooks: client.Hooks{ 60 | olog.ClientHook(), 61 | }, 62 | }) 63 | if err != nil { 64 | log.Fatalln(err) 65 | } 66 | defer c.Close() 67 | 68 | var wg sync.WaitGroup 69 | wg.Add(1) 70 | go func() { 71 | defer wg.Done() 72 | s := "testarg" 73 | ret, err := c.Test(context.Background(), hello.TestArg{S: &s}) 74 | if err != nil { 75 | log.Fatalln(err) 76 | } 77 | 78 | fmt.Printf("Test: %s, %s\n", ret.Name, ret.Dur.String()) 79 | }() 80 | 81 | ret, err := c.SayHi(context.Background(), hello.SayHiArg{Name: "Wastl", Ts: time.Now()}) 82 | if err != nil { 83 | log.Fatalln(err) 84 | } 85 | fmt.Printf("SayHi: %+v\n", ret.Res) 86 | 87 | stream, err := c.ClockTime(context.Background()) 88 | if err != nil { 89 | log.Fatalln(err) 90 | } 91 | for i := 0; i < 3; i++ { 92 | var arg hello.ClockTimeRet 93 | arg, err = stream.Read() 94 | if err != nil { 95 | if errors.Is(err, hello.ErrThisIsATest) { 96 | println("caught hello error this is a test") 97 | } else { 98 | log.Fatalln(err) 99 | } 100 | } 101 | 102 | fmt.Printf("ClockTime: %s\n", arg.Ts.String()) 103 | } 104 | 105 | bi, err := c.Bidirectional(context.Background()) 106 | if err != nil { 107 | log.Fatalln(err) 108 | } 109 | for i := 0; i < 3; i++ { 110 | err = bi.Write(hello.BidirectionalArg{Question: "What is the purpose of life?"}) 111 | if err != nil { 112 | log.Fatalln(err) 113 | } 114 | 115 | answer, err := bi.Read() 116 | if err != nil { 117 | if errors.Is(err, hello.ErrThisIsATest) { 118 | println("niranetrinaetrine") 119 | } else { 120 | log.Fatalln(err) 121 | } 122 | } 123 | 124 | fmt.Printf("Answer: %s\n", answer.Answer) 125 | } 126 | bi.Close() 127 | wg.Wait() 128 | 129 | // Open the stream and read from it. 130 | // We expect it to throw a closed error. 131 | test, err := c.TestServerCloseClientRead(context.Background()) 132 | if err != nil { 133 | fmt.Printf("ERROR(TestServerCloseClientRead): %v\n", err) 134 | return 135 | } 136 | _, err = test.Read() 137 | if err != nil { 138 | if errors.Is(err, hello.ErrClosed) { 139 | fmt.Println("SUCCESS(TestServerCloseClientRead)") 140 | } else { 141 | fmt.Printf("ERROR(TestServerCloseClientRead): %v\n", err) 142 | } 143 | } else { 144 | fmt.Println("ERROR(TestServerCloseClientRead): expected error, but got nil") 145 | } 146 | 147 | test2, err := c.TestServerContextClose(context.Background()) 148 | if err != nil { 149 | fmt.Printf("ERROR(TestServerContextClose): %v\n", err) 150 | return 151 | } 152 | err = test2.Close() 153 | if err != nil { 154 | fmt.Printf("ERROR(TestServerContextClose): close stream: %v", err) 155 | return 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /examples/simple/hello/hello.orbit: -------------------------------------------------------------------------------- 1 | version 1 2 | 3 | errors { 4 | thisIsATest = 1 5 | iAmAnError = 2 6 | } 7 | 8 | service { 9 | call sayHi { 10 | arg: { 11 | name string `validate:"required,min=1"` 12 | ts time 13 | } 14 | ret: { 15 | res []int `validate:"required,min=1"` 16 | } 17 | errors: thisIsATest 18 | } 19 | 20 | call test { 21 | async 22 | arg: { 23 | s *string 24 | } 25 | ret: { 26 | name string `validate:"required,min=1"` 27 | dur duration 28 | } 29 | timeout: 500ms 30 | maxRetSize: 10K 31 | errors: thisIsATest, iAmAnError 32 | } 33 | 34 | stream lul {} 35 | 36 | stream timeStream { 37 | arg: info 38 | } 39 | 40 | stream clockTime { 41 | ret: { 42 | ts time `validate:"required"` 43 | } 44 | errors: thisIsATest, iAmAnError 45 | } 46 | 47 | stream bidirectional { 48 | maxArgSize: 100K 49 | arg: { 50 | question string 51 | } 52 | ret: { 53 | answer string 54 | } 55 | errors: thisIsATest 56 | } 57 | 58 | stream testServerContextClose { 59 | arg: { Data []byte } 60 | } 61 | 62 | stream testServerCloseClientRead { 63 | ret: { Data []byte } 64 | } 65 | } 66 | 67 | type info { 68 | name string `validate:"required,min=1"` 69 | age int `validate:"required,min=1,max=155"` 70 | locale string `validate:"required,len=5"` 71 | address string `validate:"omitempty"` 72 | } 73 | 74 | enum vehicle { 75 | car = 1 76 | pickup = 2 77 | } 78 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/desertbit/orbit 2 | 3 | go 1.21 4 | 5 | require ( 6 | code.cloudfoundry.org/bytefmt v0.0.0-20240418163414-335139cff0b2 7 | github.com/desertbit/closer/v3 v3.5.2 8 | github.com/desertbit/grumble v1.1.3 9 | github.com/desertbit/msgp v1.1.9 10 | github.com/desertbit/yamux v1.2.0 11 | github.com/go-playground/validator/v10 v10.19.0 12 | github.com/quic-go/quic-go v0.42.0 13 | github.com/rs/zerolog v1.32.0 14 | github.com/stretchr/testify v1.9.0 15 | github.com/tinylib/msgp v1.1.9 16 | gopkg.in/vmihailenco/msgpack.v3 v3.3.3 17 | gopkg.in/yaml.v3 v3.0.1 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/desertbit/columnize v2.1.0+incompatible // indirect 23 | github.com/desertbit/go-shlex v0.1.1 // indirect 24 | github.com/desertbit/readline v1.5.1 // indirect 25 | github.com/fatih/color v1.16.0 // indirect 26 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 27 | github.com/go-playground/locales v0.14.1 // indirect 28 | github.com/go-playground/universal-translator v0.18.1 // indirect 29 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 30 | github.com/golang/protobuf v1.5.4 // indirect 31 | github.com/google/pprof v0.0.0-20240416155748-26353dc0451f // indirect 32 | github.com/kr/pretty v0.3.1 // indirect 33 | github.com/leodido/go-urn v1.4.0 // indirect 34 | github.com/mattn/go-colorable v0.1.13 // indirect 35 | github.com/mattn/go-isatty v0.0.20 // indirect 36 | github.com/onsi/ginkgo/v2 v2.17.1 // indirect 37 | github.com/philhofer/fwd v1.1.2 // indirect 38 | github.com/pmezard/go-difflib v1.0.0 // indirect 39 | github.com/quic-go/qtls-go1-20 v0.4.1 // indirect 40 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 41 | go.uber.org/mock v0.4.0 // indirect 42 | golang.org/x/crypto v0.22.0 // indirect 43 | golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect 44 | golang.org/x/mod v0.17.0 // indirect 45 | golang.org/x/net v0.24.0 // indirect 46 | golang.org/x/sys v0.19.0 // indirect 47 | golang.org/x/text v0.14.0 // indirect 48 | golang.org/x/tools v0.20.0 // indirect 49 | google.golang.org/appengine v1.6.8 // indirect 50 | google.golang.org/protobuf v1.33.0 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /grml.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project: orbit 3 | 4 | env: 5 | BINDIR: ${ROOT}/bin 6 | 7 | commands: 8 | clean: 9 | help: clean the build files 10 | exec: | 11 | chmod -R 0750 "${BINDIR}" | true 12 | rm -rf "${BINDIR}" 13 | test: 14 | help: run all go tests 15 | exec: | 16 | go test -race ./pkg/... ./internal/... ./cmd/... 17 | 18 | generate: 19 | help: generate all internal msgp code 20 | exec: | 21 | go generate ./internal/... 22 | 23 | run: 24 | help: run commands 25 | commands: 26 | simple: 27 | help: run the simple example application. 28 | commands: 29 | client: 30 | help: run the client of the simple example application. 31 | deps: 32 | - build.simple 33 | exec: | 34 | "${BINDIR}/simple-client" 35 | server: 36 | help: run the server of the simple example application. 37 | deps: 38 | - build.simple 39 | exec: | 40 | "${BINDIR}/simple-server" 41 | 42 | build: 43 | help: build commands. defaults to building everything. 44 | deps: 45 | - build.pkg 46 | - build.orbit 47 | - build.simple 48 | - build.full 49 | commands: 50 | pkg: 51 | help: build the orbit package. 52 | exec: | 53 | go build "${ROOT}/pkg/..." 54 | orbit: 55 | help: build the orbit application. 56 | exec: | 57 | go build -o "${BINDIR}/orbit" "${ROOT}/cmd/orbit/" 58 | simple: 59 | help: build the simple example application. 60 | deps: 61 | - build.orbit 62 | exec: | 63 | "${BINDIR}/orbit" gen --force "${ROOT}/examples/simple/hello/hello.orbit" 64 | go build -o "${BINDIR}/simple-client" "${ROOT}/examples/simple/client" 65 | go build -o "${BINDIR}/simple-server" "${ROOT}/examples/simple/service" 66 | full: 67 | help: build the simple example application. 68 | deps: 69 | - build.orbit 70 | exec: | 71 | "${BINDIR}/orbit" gen --force "${ROOT}/examples/full/api/api.orbit" 72 | go build -o "${BINDIR}/full-client" "${ROOT}/examples/full/client" 73 | go build -o "${BINDIR}/full-server" "${ROOT}/examples/full/server" 74 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desertbit/orbit/b95fff18eff538e60b2f9004cc0dc3e45521394e/images/logo.png -------------------------------------------------------------------------------- /images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desertbit/orbit/b95fff18eff538e60b2f9004cc0dc3e45521394e/images/logo_small.png -------------------------------------------------------------------------------- /internal/api/api.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | //go:generate msgp 29 | package api 30 | 31 | import "github.com/desertbit/orbit/pkg/codec/msgpack" 32 | 33 | const ( 34 | // The version of the orbit protocol. 35 | Version = 3 36 | ) 37 | 38 | var ( 39 | Codec = msgpack.Codec 40 | ) 41 | 42 | //#################// 43 | //### Handshake ###// 44 | //#################// 45 | 46 | type HandshakeCode int 47 | 48 | const ( 49 | HSOk HandshakeCode = 0 50 | HSInvalidVersion HandshakeCode = 1 51 | ) 52 | 53 | type HandshakeArgs struct { 54 | Version byte 55 | } 56 | 57 | type HandshakeRet struct { 58 | Code HandshakeCode 59 | SessionID string 60 | } 61 | 62 | //##############// 63 | //### Stream ###// 64 | //##############// 65 | 66 | type StreamType byte 67 | 68 | const ( 69 | StreamTypeRaw = 0 70 | StreamTypeAsyncCall = 1 71 | StreamTypeCancelCalls = 2 72 | ) 73 | 74 | type StreamRaw struct { 75 | ID string 76 | Data map[string][]byte 77 | } 78 | 79 | type StreamAsync struct { 80 | ID string 81 | } 82 | 83 | //####################// 84 | //### Typed Stream ###// 85 | //####################// 86 | 87 | type TypedStreamType byte 88 | 89 | const ( 90 | TypedStreamTypeData TypedStreamType = 0 91 | TypedStreamTypeError TypedStreamType = 1 92 | ) 93 | 94 | type TypedStreamError struct { 95 | Err string 96 | Code int 97 | } 98 | 99 | //###########// 100 | //### RPC ###// 101 | //###########// 102 | 103 | type RPCType byte 104 | 105 | const ( 106 | RPCTypeCall RPCType = 0 107 | RPCTypeReturn RPCType = 1 108 | RPCTypeCancel RPCType = 2 109 | ) 110 | 111 | type RPCCall struct { 112 | ID string 113 | Key uint32 114 | Data map[string][]byte 115 | } 116 | 117 | type RPCReturn struct { 118 | Key uint32 119 | Err string 120 | ErrCode int 121 | } 122 | 123 | type RPCCancel struct { 124 | Key uint32 125 | } 126 | -------------------------------------------------------------------------------- /internal/api/api_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package api 29 | 30 | import ( 31 | "testing" 32 | 33 | "github.com/stretchr/testify/require" 34 | "github.com/tinylib/msgp/msgp" 35 | ) 36 | 37 | func TestMsgpImplementation(t *testing.T) { 38 | t.Parallel() 39 | 40 | testCases := []interface{}{ 41 | &HandshakeArgs{}, // 0 42 | &HandshakeRet{}, 43 | &StreamRaw{}, 44 | &StreamAsync{}, 45 | &RPCCall{}, 46 | &RPCReturn{}, 47 | &RPCCancel{}, // 5 48 | } 49 | 50 | for i, tc := range testCases { 51 | require.Implements(t, (*msgp.Marshaler)(nil), tc, "case %d", i) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/bytes/bytes.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | /* 29 | Package bytes offers convenience functions to convert bytes 30 | to and from unsigned integers, respecting a defined byte-order. 31 | 32 | The underlying byte order and conversion methods being used stem 33 | from the encoding/binary package. 34 | */ 35 | package bytes 36 | 37 | import ( 38 | "encoding/binary" 39 | "errors" 40 | ) 41 | 42 | var ( 43 | // Endian defines the byte order used for the encoding. 44 | Endian binary.ByteOrder = binary.BigEndian 45 | // ErrInvalidLen is an error indicating that a byte slice had invalid length 46 | // for the conversion that should have been performed. 47 | ErrInvalidLen = errors.New("invalid byte length") 48 | ) 49 | 50 | // ToUint16 encodes the byte slice to an uint16. 51 | func ToUint16(data []byte) (v uint16, err error) { 52 | if len(data) < 2 { 53 | return 0, ErrInvalidLen 54 | } 55 | v = Endian.Uint16(data) 56 | return 57 | } 58 | 59 | // FromUint16 converts an uint16 to a byte slice. 60 | func FromUint16(v uint16) (data []byte) { 61 | data = make([]byte, 2) 62 | Endian.PutUint16(data, v) 63 | return 64 | } 65 | 66 | // ToUint32 encodes the byte slice to an uint32. 67 | func ToUint32(data []byte) (v uint32, err error) { 68 | if len(data) < 4 { 69 | return 0, ErrInvalidLen 70 | } 71 | v = Endian.Uint32(data) 72 | return 73 | } 74 | 75 | // FromUint32 converts an uint32 to a byte slice. 76 | func FromUint32(v uint32) (data []byte) { 77 | data = make([]byte, 4) 78 | Endian.PutUint32(data, v) 79 | return 80 | } 81 | 82 | // ToUint64 encodes the byte slice to an uint64. 83 | func ToUint64(data []byte) (v uint64, err error) { 84 | if len(data) < 8 { 85 | return 0, ErrInvalidLen 86 | } 87 | v = Endian.Uint64(data) 88 | return 89 | } 90 | 91 | // FromUint64 converts an uint64 to a byte slice. 92 | func FromUint64(v uint64) (data []byte) { 93 | data = make([]byte, 8) 94 | Endian.PutUint64(data, v) 95 | return 96 | } 97 | -------------------------------------------------------------------------------- /internal/bytes/bytes_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package bytes_test 29 | 30 | import ( 31 | "testing" 32 | 33 | "github.com/desertbit/orbit/internal/bytes" 34 | "github.com/stretchr/testify/require" 35 | ) 36 | 37 | func TestUInt16Conversion(t *testing.T) { 38 | uint16Max := uint16(1<<16 - 1) 39 | numbers := []uint16{5251, uint16Max, 0, 1, 101, 2387, 219} 40 | 41 | for _, i := range numbers { 42 | data := bytes.FromUint16(i) 43 | require.Len(t, data, 2) 44 | 45 | ii, err := bytes.ToUint16(data) 46 | require.NoError(t, err) 47 | require.Equal(t, i, ii) 48 | } 49 | 50 | ii, err := bytes.ToUint16(nil) 51 | require.Equal(t, bytes.ErrInvalidLen, err) 52 | require.EqualValues(t, 0, ii) 53 | 54 | ii, err = bytes.ToUint16(make([]byte, 1)) 55 | require.Equal(t, bytes.ErrInvalidLen, err) 56 | require.EqualValues(t, 0, ii) 57 | } 58 | 59 | func TestUInt32Conversion(t *testing.T) { 60 | uint32Max := uint32(1<<32 - 1) 61 | numbers := []uint32{5251, uint32Max, 0, 1, 101, 2387, 219} 62 | 63 | for _, i := range numbers { 64 | data := bytes.FromUint32(i) 65 | require.Len(t, data, 4) 66 | 67 | ii, err := bytes.ToUint32(data) 68 | require.NoError(t, err) 69 | require.Equal(t, i, ii) 70 | } 71 | 72 | ii, err := bytes.ToUint32(nil) 73 | require.Equal(t, bytes.ErrInvalidLen, err) 74 | require.EqualValues(t, 0, ii) 75 | 76 | ii, err = bytes.ToUint32(make([]byte, 1)) 77 | require.Equal(t, bytes.ErrInvalidLen, err) 78 | require.EqualValues(t, 0, ii) 79 | } 80 | 81 | func TestUInt64Conversion(t *testing.T) { 82 | uint64Max := uint64(1<<64 - 1) 83 | numbers := []uint64{5251, uint64Max, 0, 1, 101, 2387, 219, 213613871263} 84 | 85 | for _, i := range numbers { 86 | data := bytes.FromUint64(i) 87 | require.Len(t, data, 8) 88 | 89 | ii, err := bytes.ToUint64(data) 90 | require.NoError(t, err) 91 | require.Equal(t, i, ii) 92 | } 93 | 94 | ii, err := bytes.ToUint64(nil) 95 | require.Equal(t, bytes.ErrInvalidLen, err) 96 | require.EqualValues(t, 0, ii) 97 | 98 | ii, err = bytes.ToUint64(make([]byte, 1)) 99 | require.Equal(t, bytes.ErrInvalidLen, err) 100 | require.EqualValues(t, 0, ii) 101 | } 102 | -------------------------------------------------------------------------------- /internal/codegen/ast/ast.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package ast 29 | 30 | import ( 31 | "time" 32 | 33 | "github.com/desertbit/orbit/internal/codegen/lexer" 34 | "github.com/desertbit/orbit/internal/strutil" 35 | ) 36 | 37 | type File struct { 38 | Version *Version 39 | Srvc *Service 40 | Types []*Type 41 | Errs []*Error 42 | Enums []*Enum 43 | } 44 | 45 | type Version struct { 46 | Value int 47 | lexer.Pos 48 | } 49 | 50 | type Enum struct { 51 | Name string 52 | Values []*EnumValue 53 | lexer.Pos 54 | } 55 | 56 | func (e Enum) Ident() string { 57 | return strutil.FirstUpper(e.Name) 58 | } 59 | 60 | type EnumValue struct { 61 | Name string 62 | Value int 63 | lexer.Pos 64 | } 65 | 66 | func (ev EnumValue) Ident() string { 67 | return strutil.FirstUpper(ev.Name) 68 | } 69 | 70 | type Error struct { 71 | Name string 72 | ID int 73 | lexer.Pos 74 | } 75 | 76 | func (e Error) Ident() string { 77 | return strutil.FirstUpper(e.Name) 78 | } 79 | 80 | type Type struct { 81 | Name string 82 | Fields []*TypeField 83 | lexer.Pos 84 | } 85 | 86 | func (t Type) Ident() string { 87 | return strutil.FirstUpper(t.Name) 88 | } 89 | 90 | type TypeField struct { 91 | Name string 92 | DataType DataType 93 | StructTag string 94 | lexer.Pos 95 | } 96 | 97 | func (tf TypeField) Ident() string { 98 | return strutil.FirstUpper(tf.Name) 99 | } 100 | 101 | type Service struct { 102 | Calls []*Call 103 | Streams []*Stream 104 | lexer.Pos 105 | } 106 | 107 | type Call struct { 108 | Name string 109 | Arg DataType 110 | Ret DataType 111 | Async bool 112 | Timeout *time.Duration 113 | MaxArgSize *int64 114 | MaxRetSize *int64 115 | Errors []*Error 116 | lexer.Pos 117 | } 118 | 119 | func (c *Call) Ident() string { 120 | return strutil.FirstUpper(c.Name) 121 | } 122 | 123 | func (c *Call) IdentPrv() string { 124 | return strutil.FirstLower(c.Name) 125 | } 126 | 127 | type Stream struct { 128 | Name string 129 | Arg DataType 130 | Ret DataType 131 | MaxArgSize *int64 132 | MaxRetSize *int64 133 | Errors []*Error 134 | lexer.Pos 135 | } 136 | 137 | func (s *Stream) Ident() string { 138 | return strutil.FirstUpper(s.Name) 139 | } 140 | 141 | func (s *Stream) IdentPrv() string { 142 | return strutil.FirstLower(s.Name) 143 | } 144 | -------------------------------------------------------------------------------- /internal/codegen/ast/datatype.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package ast 29 | 30 | import ( 31 | "github.com/desertbit/orbit/internal/codegen/lexer" 32 | "github.com/desertbit/orbit/internal/strutil" 33 | ) 34 | 35 | const ( 36 | TypeByte = "byte" 37 | TypeString = "string" 38 | TypeTime = "time" 39 | TypeDuration = "duration" 40 | 41 | TypeBool = "bool" 42 | 43 | TypeInt = "int" 44 | TypeInt8 = "int8" 45 | TypeInt16 = "int16" 46 | TypeInt32 = "int32" 47 | TypeInt64 = "int64" 48 | 49 | TypeUInt = "uint" 50 | TypeUInt8 = "uint8" 51 | TypeUInt16 = "uint16" 52 | TypeUInt32 = "uint32" 53 | TypeUInt64 = "uint64" 54 | 55 | TypeFloat32 = "float32" 56 | TypeFloat64 = "float64" 57 | ) 58 | 59 | type DataType interface { 60 | // Returns go variable declaration. 61 | Decl() string 62 | // Returns identifier. 63 | ID() string 64 | // Pos returns the lexer position. 65 | Pos() lexer.Pos 66 | } 67 | 68 | type dataType struct { 69 | pos lexer.Pos 70 | pointer bool 71 | } 72 | 73 | func (d dataType) declBase() string { 74 | if d.pointer { 75 | return "*" 76 | } else { 77 | return "" 78 | } 79 | } 80 | 81 | func (d dataType) Pos() lexer.Pos { 82 | return d.pos 83 | } 84 | 85 | func (d dataType) Pointer() bool { 86 | return d.pointer 87 | } 88 | 89 | func newDataType(pos lexer.Pos, pointer bool) dataType { 90 | return dataType{ 91 | pos: pos, 92 | pointer: pointer, 93 | } 94 | } 95 | 96 | type BaseType struct { 97 | dataType 98 | 99 | DataType string 100 | } 101 | 102 | func NewBaseType(dataType string, pos lexer.Pos, pointer bool) *BaseType { 103 | return &BaseType{ 104 | DataType: dataType, 105 | dataType: newDataType(pos, pointer), 106 | } 107 | } 108 | 109 | func (b *BaseType) Decl() string { 110 | d := b.declBase() 111 | switch b.DataType { 112 | case TypeTime: 113 | d += "time.Time" 114 | case TypeDuration: 115 | d += "time.Duration" 116 | default: 117 | d += b.DataType 118 | } 119 | return d 120 | } 121 | 122 | func (b *BaseType) ID() string { 123 | return b.DataType 124 | } 125 | 126 | type MapType struct { 127 | dataType 128 | 129 | Key DataType 130 | Value DataType 131 | } 132 | 133 | func NewMapType(key, value DataType, pos lexer.Pos, pointer bool) *MapType { 134 | return &MapType{ 135 | dataType: newDataType(pos, pointer), 136 | Key: key, 137 | Value: value, 138 | } 139 | } 140 | 141 | func (m *MapType) Decl() string { 142 | return m.declBase() + "map[" + m.Key.Decl() + "]" + m.Value.Decl() 143 | } 144 | 145 | func (m *MapType) ID() string { 146 | return m.Decl() 147 | } 148 | 149 | type ArrType struct { 150 | dataType 151 | 152 | Elem DataType 153 | } 154 | 155 | func NewArrType(elem DataType, pos lexer.Pos, pointer bool) *ArrType { 156 | return &ArrType{ 157 | dataType: newDataType(pos, pointer), 158 | Elem: elem, 159 | } 160 | } 161 | 162 | func (a *ArrType) Decl() string { 163 | return a.declBase() + "[]" + a.Elem.Decl() 164 | } 165 | 166 | func (a *ArrType) ID() string { 167 | return a.Decl() 168 | } 169 | 170 | type StructType struct { 171 | dataType 172 | 173 | Name string 174 | } 175 | 176 | func NewStructType(name string, pos lexer.Pos, pointer bool) *StructType { 177 | return &StructType{ 178 | dataType: newDataType(pos, pointer), 179 | Name: name, 180 | } 181 | } 182 | 183 | func (s *StructType) Decl() string { 184 | return s.declBase() + strutil.FirstUpper(s.Name) 185 | } 186 | 187 | func (s *StructType) ID() string { 188 | return s.Name 189 | } 190 | 191 | type EnumType struct { 192 | dataType 193 | 194 | Name string 195 | } 196 | 197 | func NewEnumType(name string, pos lexer.Pos, pointer bool) *EnumType { 198 | return &EnumType{ 199 | dataType: newDataType(pos, pointer), 200 | Name: name, 201 | } 202 | } 203 | 204 | func (e *EnumType) Decl() string { 205 | return e.declBase() + strutil.FirstUpper(e.Name) 206 | } 207 | 208 | func (e *EnumType) ID() string { 209 | return e.Name 210 | } 211 | 212 | type AnyType struct { 213 | dataType 214 | 215 | Name string 216 | } 217 | 218 | func NewAnyType(name string, pos lexer.Pos, pointer bool) *AnyType { 219 | return &AnyType{ 220 | dataType: newDataType(pos, pointer), 221 | Name: name, 222 | } 223 | } 224 | 225 | func (a *AnyType) Decl() string { 226 | return "unresolved any type" + a.declBase() 227 | } 228 | 229 | func (a *AnyType) ID() string { 230 | return a.Name 231 | } 232 | -------------------------------------------------------------------------------- /internal/codegen/ast/error.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package ast 29 | 30 | import ( 31 | "fmt" 32 | ) 33 | 34 | func NewErr(line int, format string, args ...interface{}) error { 35 | return Err{ 36 | msg: fmt.Sprintf(format, args...), 37 | line: line, 38 | } 39 | } 40 | 41 | type Err struct { 42 | msg string 43 | line int 44 | } 45 | 46 | func (e Err) Error() string { 47 | return fmt.Sprintf("%s --- line: %d", e.msg, e.line) 48 | } 49 | -------------------------------------------------------------------------------- /internal/codegen/gen/cache.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package gen 29 | 30 | import ( 31 | "errors" 32 | "fmt" 33 | "io/ioutil" 34 | "os" 35 | "path/filepath" 36 | "strings" 37 | "time" 38 | 39 | "github.com/desertbit/orbit/internal/codegen" 40 | yaml "gopkg.in/yaml.v3" 41 | ) 42 | 43 | const ( 44 | cacheDirName = "orbit" 45 | modTimesFile = "mod_times" 46 | ) 47 | 48 | var ( 49 | errCacheNotFound = errors.New("gen cache not found") 50 | errCacheInvalid = errors.New("gen cache is invalid") 51 | ) 52 | 53 | type cacheEntry struct { 54 | LastModified time.Time `yaml:"last-modified"` 55 | Version int `yaml:"version"` 56 | } 57 | 58 | // compareWithGenCache compares the file at the given path against the gen cache 59 | // and returns true, if the file is newer. 60 | // Returns errCacheNotFound, if no cache could be found. 61 | // Returns errCacheInvalid, if the cache found was invalid. 62 | func compareWithGenCache(orbitFile string, force bool) (modified bool, err error) { 63 | gc, _, err := loadGenCache() 64 | if err != nil { 65 | return 66 | } 67 | gcEntry, foundInCache := gc[orbitFile] 68 | 69 | // Retrieve the file's info. 70 | fi, err := os.Lstat(orbitFile) 71 | if err != nil { 72 | return 73 | } 74 | 75 | // Check, if the file has been generated before. 76 | genFileExists, err := fileExists(strings.Replace(orbitFile, orbitSuffix, genOrbitSuffix, 1)) 77 | if err != nil { 78 | return 79 | } 80 | 81 | // The file counts as modified: 82 | modified = !foundInCache || // if it is not found in the cache or, 83 | !gcEntry.LastModified.Equal(fi.ModTime()) || // if its last modification timestamp does not match the cached modification time or, 84 | !genFileExists || // if its generated file does not exist or, 85 | gcEntry.Version != codegen.CacheVersion || // if its version does not match the current cache version or, 86 | force // if force is enabled. 87 | return 88 | } 89 | 90 | // updateGenCache updates the gen cache on disk for the given file. 91 | func updateGenCache(orbitFile string) (err error) { 92 | // Load the current gen cache. 93 | gc, cacheDir, err := loadGenCache() 94 | if err != nil { 95 | if errors.Is(err, errCacheInvalid) || errors.Is(err, errCacheNotFound) { 96 | // Create/overwrite the cache. 97 | gc = make(map[string]cacheEntry) 98 | err = nil 99 | } else { 100 | return 101 | } 102 | } 103 | 104 | // Retrieve the file's info. 105 | fi, err := os.Lstat(orbitFile) 106 | if err != nil { 107 | return 108 | } 109 | 110 | // Update the cache. 111 | gc[orbitFile] = cacheEntry{LastModified: fi.ModTime(), Version: codegen.CacheVersion} 112 | 113 | data, err := yaml.Marshal(gc) 114 | if err != nil { 115 | return 116 | } 117 | 118 | return ioutil.WriteFile(filepath.Join(cacheDir, modTimesFile), data, filePerm) 119 | } 120 | 121 | // If one of the predefined errors is returned, the cacheDir is guaranteed to 122 | // be set to a valid value. 123 | // Returns errCacheNotFound, if no cache could be found. 124 | // Returns errCacheInvalid, if the cache found was invalid. 125 | func loadGenCache() (gc map[string]cacheEntry, cacheDir string, err error) { 126 | // Get the cache dir. 127 | cacheDir, err = os.UserCacheDir() 128 | if err != nil { 129 | return 130 | } 131 | 132 | // Ensure, our directory exists. 133 | cacheDir = filepath.Join(cacheDir, cacheDirName) 134 | err = os.MkdirAll(cacheDir, dirPerm) 135 | if err != nil { 136 | return 137 | } 138 | 139 | // Read the data from the cache file. 140 | data, err := ioutil.ReadFile(filepath.Join(cacheDir, modTimesFile)) 141 | if err != nil { 142 | if errors.Is(err, os.ErrNotExist) { 143 | err = errCacheNotFound 144 | } 145 | return 146 | } 147 | 148 | // Parse it to our struct using yaml. 149 | gc = make(map[string]cacheEntry) 150 | err = yaml.Unmarshal(data, &gc) 151 | if err != nil { 152 | err = fmt.Errorf("%w: %v", errCacheInvalid, err) 153 | return 154 | } 155 | 156 | return 157 | } 158 | -------------------------------------------------------------------------------- /internal/codegen/gen/gen_enum.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package gen 29 | 30 | import ( 31 | "sort" 32 | 33 | "github.com/desertbit/orbit/internal/codegen/ast" 34 | ) 35 | 36 | func (g *generator) genEnums(enums []*ast.Enum) { 37 | // Sort the enums in lexicographical order. 38 | sort.Slice(enums, func(i, j int) bool { 39 | return enums[i].Name < enums[j].Name 40 | }) 41 | 42 | for _, en := range enums { 43 | g.writefLn("type %s int", en.Ident()) 44 | g.writeLn("const (") 45 | for _, env := range en.Values { 46 | g.writefLn("%s%s %s = %d", en.Ident(), env.Ident(), en.Ident(), env.Value) 47 | } 48 | g.writeLn(")") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/codegen/gen/gen_error.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package gen 29 | 30 | import ( 31 | "sort" 32 | 33 | "github.com/desertbit/orbit/internal/codegen/ast" 34 | ) 35 | 36 | const ( 37 | valErrorCheck = "_valErrCheck" 38 | ) 39 | 40 | func (g *generator) genErrors(errs []*ast.Error) { 41 | // Sort the errors in alphabetical order. 42 | sort.Slice(errs, func(i, j int) bool { 43 | return errs[i].Name < errs[j].Name 44 | }) 45 | 46 | // Write error codes. 47 | if len(errs) > 0 { 48 | g.writeLn("const (") 49 | for _, e := range errs { 50 | g.writefLn("ErrCode%s = %d", e.Ident(), e.ID) 51 | } 52 | g.writeLn(")") 53 | 54 | // Write standard error variables. 55 | g.writeLn("var (") 56 | for _, e := range errs { 57 | g.writefLn("Err%s = errors.New(\"%s\")", e.Ident(), strExplode(e.Ident())) 58 | } 59 | g.writeLn(")") 60 | g.writeLn("") 61 | } 62 | 63 | // Generate error check helper funcs. 64 | g.genValErrCheckFunc() 65 | } 66 | 67 | func (g *generator) genClientErrorInlineCheck(errs []*ast.Error) { 68 | g.writeLn("var cErr oclient.Error") 69 | g.writeLn("if errors.As(err, &cErr) {") 70 | g.writeLn("switch cErr.Code() {") 71 | for _, e := range errs { 72 | g.writefLn("case ErrCode%s:", e.Ident()) 73 | g.writefLn("err = Err%s", e.Ident()) 74 | } 75 | g.writeLn("}") 76 | g.writeLn("}") 77 | g.writeLn("return") 78 | } 79 | 80 | func (g *generator) genServiceErrorInlineCheck(errs []*ast.Error) { 81 | for i, e := range errs { 82 | g.writefLn("if errors.Is(err, Err%s) {", e.Ident()) 83 | g.writefLn("err = oservice.NewError(err, Err%s.Error(), ErrCode%s)", e.Ident(), e.Ident()) 84 | if i < len(errs)-1 { 85 | g.write("} else ") 86 | } else { 87 | g.writeLn("}") 88 | } 89 | } 90 | g.writeLn("return") 91 | } 92 | 93 | func (g *generator) genValErrCheckFunc() { 94 | g.writefLn("func %s(err error) error {", valErrorCheck) 95 | g.writeLn("if vErrs, ok := err.(validator.ValidationErrors); ok {") 96 | g.writeLn("var errMsg strings.Builder") 97 | g.writeLn("for _, err := range vErrs {") 98 | g.writeLn("errMsg.WriteString(fmt.Sprintf(\"[name: '%s', value: '%s', tag: '%s']\", err.StructNamespace(), err.Value(), err.Tag()))") 99 | g.writeLn("}") 100 | g.writeLn("return errors.New(errMsg.String())") 101 | g.writeLn("}") 102 | g.writeLn("return err") 103 | g.writeLn("}") 104 | g.writeLn("") 105 | } 106 | -------------------------------------------------------------------------------- /internal/codegen/gen/gen_service_stream.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package gen 29 | 30 | import ( 31 | "github.com/desertbit/orbit/internal/codegen/ast" 32 | ) 33 | 34 | //##############// 35 | //### Client ###// 36 | //##############// 37 | 38 | func (g *generator) genClientStreamSignature(s *ast.Stream) { 39 | if s.Arg == nil && s.Ret == nil { 40 | // Raw. 41 | g.writef("%s(ctx context.Context) (stream transport.Stream, err error)", s.Ident()) 42 | } else { 43 | // Typed. 44 | g.writef("%s(ctx context.Context) (stream *%sClientStream, err error)", s.Ident(), s.Ident()) 45 | } 46 | } 47 | 48 | func (g *generator) genClientStream(s *ast.Stream) { 49 | // Method declaration. 50 | g.writef("func (%s *client) ", recv) 51 | g.genClientStreamSignature(s) 52 | g.writeLn(" {") 53 | 54 | // Ensure Timeout on context. 55 | g.writefLn("if %s.streamInitTimeout > 0 {", recv) 56 | g.writeLn("var cancel context.CancelFunc") 57 | g.writefLn("ctx, cancel = context.WithTimeout(ctx, %s.streamInitTimeout)", recv) 58 | g.writeLn("defer cancel()") 59 | g.writeLn("}") 60 | 61 | // Implementation. 62 | if s.Arg == nil && s.Ret == nil { 63 | // Raw. 64 | g.writefLn("stream, err = %s.Stream(ctx, StreamID%s)", recv, s.Ident()) 65 | g.errIfNil() 66 | } else { 67 | // Typed. 68 | g.writef("str, err := %s.%s(ctx, StreamID%s,", recv, typedStream(s, false), s.Ident()) 69 | if s.Arg != nil { 70 | g.writeOrbitMaxSizeParam(s.MaxArgSize, false) 71 | } 72 | if s.Ret != nil { 73 | g.writeOrbitMaxSizeParam(s.MaxRetSize, false) 74 | } 75 | g.writeLn(")") 76 | g.errIfNil() 77 | g.writefLn("stream = new%sClientStream(str)", s.Ident()) 78 | } 79 | 80 | // End of method. 81 | g.writeLn("return") 82 | g.writeLn("}") 83 | g.writeLn("") 84 | } 85 | 86 | //###############// 87 | //### Service ###// 88 | //###############// 89 | 90 | func (g *generator) genServiceStreamRegister(s *ast.Stream) { 91 | if s.Arg == nil && s.Ret == nil { 92 | // Raw. 93 | g.writefLn("os.RegisterStream(StreamID%s, srvc.%s)", s.Ident(), s.IdentPrv()) 94 | } else { 95 | // Typed. 96 | g.writef("os.Register%s(StreamID%s, srvc.%s,", typedStream(s, true), s.Ident(), s.IdentPrv()) 97 | if s.Arg != nil { 98 | g.writeOrbitMaxSizeParam(s.MaxArgSize, true) 99 | } 100 | if s.Ret != nil { 101 | g.writeOrbitMaxSizeParam(s.MaxRetSize, true) 102 | } 103 | g.writeLn(")") 104 | } 105 | } 106 | 107 | func (g *generator) genServiceHandlerStreamSignature(s *ast.Stream) { 108 | g.writef("%s(ctx oservice.Context, ", s.Ident()) 109 | 110 | if s.Arg == nil && s.Ret == nil { 111 | // Raw. 112 | g.writeLn("stream transport.Stream)") 113 | } else { 114 | // Typed. 115 | g.writefLn("stream *%sServiceStream) error", s.Ident()) 116 | } 117 | } 118 | 119 | func (g *generator) genServiceStream(s *ast.Stream) { 120 | g.writef("func (%s *service) %s(ctx oservice.Context, ", recv, s.IdentPrv()) 121 | 122 | if s.Arg == nil && s.Ret == nil { 123 | // Raw. 124 | g.writeLn("stream transport.Stream) {") 125 | g.writefLn("%s.h.%s(ctx, stream)", recv, s.Ident()) 126 | } else { 127 | // Typed. 128 | g.writefLn("stream oservice.%s) (err error) {", typedStream(s, true)) 129 | g.writefLn("err = %s.h.%s(ctx, new%sServiceStream(stream))", recv, s.Ident(), s.Ident()) 130 | g.errIfNilFunc(func() { 131 | if len(s.Errors) != 0 { 132 | // Inline check for defined errors. 133 | g.genServiceErrorInlineCheck(s.Errors) 134 | } else { 135 | g.writeLn("return") 136 | } 137 | }) 138 | g.writeLn("return") 139 | } 140 | 141 | g.writeLn("}") 142 | g.writeLn("") 143 | } 144 | 145 | //###############// 146 | //### Private ###// 147 | //###############// 148 | 149 | func typedStream(s *ast.Stream, service bool) (dataType string) { 150 | if s.Arg != nil && s.Ret != nil { 151 | // ReadWrite. 152 | return "TypedRWStream" 153 | } else if (!service && s.Ret != nil) || (service && s.Arg != nil) { 154 | // Read. 155 | return "TypedRStream" 156 | } else { 157 | // Write. 158 | return "TypedWStream" 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /internal/codegen/gen/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package gen 29 | 30 | import ( 31 | "errors" 32 | "fmt" 33 | "os" 34 | "os/exec" 35 | "unicode" 36 | ) 37 | 38 | // execCmd executes the given command with the given arguments. 39 | // It checks for exec specific ExitErrors and annotates them. 40 | func execCmd(name string, args ...string) (err error) { 41 | cmd := exec.Command(name, args...) 42 | _, err = cmd.Output() 43 | if err != nil { 44 | var eErr *exec.ExitError 45 | if errors.As(err, &eErr) { 46 | return fmt.Errorf("%s: %v", name, string(eErr.Stderr)) 47 | } 48 | return 49 | } 50 | return 51 | } 52 | 53 | // fileExists checks, if the file at the given path exists. 54 | // Returns true, if the file exists, else false and optionally an error. 55 | func fileExists(path string) (bool, error) { 56 | _, err := os.Lstat(path) 57 | if err == nil { 58 | return true, nil 59 | } 60 | if os.IsNotExist(err) { 61 | return false, nil 62 | } 63 | return false, err 64 | } 65 | 66 | // strExplode splits up s, so that every uppercase rune is converted to lowercase 67 | // and gets prepended a space. 68 | // If multiple uppercase runes follow each other, they are seen as one abbreviation. 69 | // Example: "DNNBenchmark" -> "dnn benchmark" 70 | func strExplode(s string) string { 71 | n := make([]rune, 0, len([]rune(s))) 72 | 73 | var ( 74 | lr rune 75 | prevWasUpper bool 76 | isUpperSeq bool 77 | ) 78 | 79 | prevIsNoSpace := func() bool { return len(n) > 0 && n[len(n)-1] != ' ' } 80 | 81 | for _, r := range s { 82 | if unicode.IsSpace(r) { 83 | n = append(n, r) 84 | prevWasUpper = false 85 | isUpperSeq = false 86 | } else if unicode.IsUpper(r) { 87 | if prevWasUpper { 88 | if !isUpperSeq && prevIsNoSpace() { 89 | n = append(n, ' ') 90 | } 91 | n = append(n, lr) 92 | } 93 | 94 | lr = unicode.ToLower(r) 95 | 96 | if prevWasUpper { 97 | isUpperSeq = true 98 | } 99 | prevWasUpper = true 100 | } else { 101 | if prevWasUpper { 102 | if prevIsNoSpace() { 103 | n = append(n, ' ') 104 | } 105 | n = append(n, lr) 106 | } 107 | 108 | n = append(n, r) 109 | prevWasUpper = false 110 | isUpperSeq = false 111 | } 112 | } 113 | 114 | // Last rune was uppercase. 115 | if prevWasUpper { 116 | if !isUpperSeq && prevIsNoSpace() { 117 | n = append(n, ' ') 118 | } 119 | n = append(n, lr) 120 | } 121 | 122 | return string(n) 123 | } 124 | -------------------------------------------------------------------------------- /internal/codegen/gen/utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package gen 29 | 30 | import ( 31 | "testing" 32 | 33 | r "github.com/stretchr/testify/require" 34 | ) 35 | 36 | func TestStrExplode(t *testing.T) { 37 | cases := []struct { 38 | src string 39 | expect string 40 | }{ 41 | { 42 | src: "", 43 | expect: "", 44 | }, 45 | { 46 | src: "hello", 47 | expect: "hello", 48 | }, 49 | { 50 | src: "hEllo", 51 | expect: "h ello", 52 | }, 53 | { 54 | src: "Hello", 55 | expect: "hello", 56 | }, 57 | { 58 | src: "Hello hello", 59 | expect: "hello hello", 60 | }, 61 | { 62 | src: "Hello HellO", 63 | expect: "hello hell o", 64 | }, 65 | { 66 | src: "Hello HellOO", 67 | expect: "hello hell oo", 68 | }, 69 | { 70 | src: "Hello O", 71 | expect: "hello o", 72 | }, 73 | { 74 | src: "Hello Hell OO", 75 | expect: "hello hell oo", 76 | }, 77 | { 78 | src: "HELlo", 79 | expect: "he llo", 80 | }, 81 | { 82 | src: "sashimI", 83 | expect: "sashim i", 84 | }, 85 | } 86 | 87 | for i, c := range cases { 88 | r.Equal(t, c.expect, strExplode(c.src), "test case %d", i) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /internal/codegen/lexer/states.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package lexer 29 | 30 | import ( 31 | "strconv" 32 | "unicode" 33 | ) 34 | 35 | func lexTokenStart(l *lexer) stateFn { 36 | var ( 37 | r rune 38 | t TokenType 39 | ) 40 | 41 | // Discard leading whitespace. 42 | for r = l.next(); r != eof; r = l.next() { 43 | if !unicode.IsSpace(r) { 44 | l.backup() 45 | break 46 | } 47 | } 48 | l.ignore() 49 | 50 | // Retrieve the first rune and check, 51 | // if it is a special symbol we must 52 | // emit a token for separately. 53 | r = l.next() 54 | 55 | // Number. 56 | if r == hyphen || unicode.IsDigit(r) { 57 | l.backup() 58 | return lexNumber 59 | } 60 | 61 | // Delimiter. 62 | t = toDelimTokenType(r) 63 | if t != ILLEGAL { 64 | l.emit(t) 65 | return lexTokenStart 66 | } 67 | 68 | // Raw string. 69 | if r == backtick { 70 | l.ignore() 71 | return lexRawString 72 | } 73 | 74 | // Comments. 75 | if r == slash { 76 | // May be a comment. 77 | nr := l.next() 78 | if nr == slash { 79 | return lexLineComment 80 | } else if nr == asterisk { 81 | return lexBlockComment 82 | } 83 | // No comment. 84 | l.backup() 85 | } 86 | 87 | return lexTokenFindEnd 88 | } 89 | 90 | func lexTokenFindEnd(l *lexer) stateFn { 91 | // Find next boundary. 92 | for r := l.next(); r != eof; r = l.next() { 93 | if unicode.IsSpace(r) || isDelim(r) || r == backtick { 94 | l.backup() 95 | break 96 | } 97 | } 98 | 99 | // Valid EOF. 100 | if !l.pendingInput() { 101 | return nil 102 | } 103 | 104 | // Check for keywords. 105 | t := toKeywordTokenType(l.curInput()) 106 | if t != ILLEGAL { 107 | l.emit(t) 108 | } else { 109 | // Must be identifier. 110 | l.emit(IDENT) 111 | } 112 | 113 | return lexTokenStart 114 | } 115 | 116 | func lexRawString(l *lexer) stateFn { 117 | // Collect input until next rquote. 118 | // Newlines are an error for raw strings. 119 | for r := l.next(); r != eof; r = l.next() { 120 | if r == newline { 121 | return l.errorf("unexpected newline in raw string literal") 122 | } else if r == backtick { 123 | // End of string. 124 | // Remove rquote from input. 125 | l.backup() 126 | // Publish input. 127 | l.emit(RAWSTRING) 128 | // Ignore rquote. 129 | l.next() 130 | l.ignore() 131 | return lexTokenStart 132 | } 133 | } 134 | 135 | // Invalid EOF. 136 | return l.errorf("unterminated raw string literal") 137 | } 138 | 139 | func lexNumber(l *lexer) stateFn { 140 | // Collect input until next space or eof. 141 | var ( 142 | r rune 143 | onlyDigits = true 144 | ) 145 | for r = l.next(); r != eof; r = l.next() { 146 | if unicode.IsSpace(r) { 147 | // Do not include space in input. 148 | l.backup() 149 | break 150 | } 151 | 152 | if r != hyphen && !unicode.IsDigit(r) { 153 | onlyDigits = false 154 | } else if !onlyDigits { 155 | // After a non-digit, no digit can follow. 156 | return l.errorf("invalid number literal: '%s'", l.curInput()) 157 | } 158 | } 159 | 160 | if !l.pendingInput() && r == eof { 161 | // Invalid EOF. 162 | return l.errorf("unterminated number literal") 163 | } 164 | ci := l.curInput() 165 | 166 | if onlyDigits { 167 | // Integer, use strconv package. 168 | _, err := strconv.Atoi(ci) 169 | if err != nil { 170 | return l.errorf("invalid number literal: %v", err) 171 | } 172 | 173 | // Publish int. 174 | l.emit(INT) 175 | return lexTokenStart 176 | } 177 | 178 | // Treat a number with digits followed by chars as ident. 179 | l.emit(IDENT) 180 | return lexTokenStart 181 | } 182 | 183 | func lexLineComment(l *lexer) stateFn { 184 | // Consume tokens until a newline or eof is encountered. 185 | for r := l.next(); r != eof && r != newline; r = l.next() { 186 | } 187 | l.ignore() 188 | return lexTokenStart 189 | } 190 | 191 | func lexBlockComment(l *lexer) stateFn { 192 | // Consume tokens until block comment end or eof is encountered. 193 | for r := l.next(); r != eof; r = l.next() { 194 | if r == asterisk { 195 | r = l.next() 196 | if r == slash { 197 | l.ignore() 198 | return lexTokenStart 199 | } 200 | } 201 | } 202 | 203 | // Valid EOF. 204 | l.ignore() 205 | return nil 206 | } 207 | -------------------------------------------------------------------------------- /internal/codegen/lexer/token.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package lexer 29 | 30 | // TokenType identifies the type of lex tokens. 31 | type TokenType int 32 | 33 | const ( 34 | // Special Tokens 35 | 36 | ILLEGAL TokenType = iota 37 | EOF 38 | 39 | // Literals 40 | 41 | literalBegin 42 | IDENT // sendData 43 | INT // 123 44 | RAWSTRING // `rawstring` 45 | literalEnd 46 | 47 | // Keywords 48 | 49 | keywordBegin 50 | VERSION 51 | ERRORS 52 | ENUM 53 | TYPE 54 | SERVICE 55 | CALL 56 | STREAM 57 | ASYNC 58 | ARG 59 | RET 60 | MAXARGSIZE 61 | MAXRETSIZE 62 | TIMEOUT 63 | MAP 64 | keywordEnd 65 | 66 | // Delimiters 67 | 68 | delimBegin 69 | LPAREN // ( 70 | RPAREN // ) 71 | LBRACE // { 72 | RBRACE // } 73 | LBRACK // [ 74 | RBRACK // ] 75 | COLON // : 76 | EQUAL // = 77 | COMMA // , 78 | ASTERISK // * 79 | delimEnd 80 | ) 81 | 82 | func (tt TokenType) String() string { 83 | if tt == ILLEGAL { 84 | return "illegal" 85 | } else if tt == EOF { 86 | return "eof" 87 | } else if literalBegin < tt && tt < literalEnd { 88 | switch tt { 89 | case IDENT: 90 | return "ident" 91 | case INT: 92 | return "int" 93 | case RAWSTRING: 94 | return "rawstring" 95 | } 96 | } else if keywordBegin < tt && tt < keywordEnd { 97 | for k, v := range keywordTokenTypes { 98 | if v == tt { 99 | return k 100 | } 101 | } 102 | } else { 103 | for k, v := range delimTokenTypes { 104 | if v == tt { 105 | return string(k) 106 | } 107 | } 108 | } 109 | 110 | return "__UNKNOWN__" 111 | } 112 | 113 | // Token is an item returned from the scanner. 114 | type Token struct { 115 | Type TokenType 116 | Value string 117 | Pos Pos 118 | } 119 | 120 | // IsLiteral returns true, if the token has 121 | // type literal. 122 | func (t Token) IsLiteral() bool { 123 | return literalBegin < t.Type && t.Type < literalEnd 124 | } 125 | 126 | // IsKeyword returns true, if the token has 127 | // type keyword. 128 | func (t Token) IsKeyword() bool { 129 | return keywordBegin < t.Type && t.Type < keywordEnd 130 | } 131 | 132 | // IsDelimiter returns true, if the token has 133 | // type delimiter. 134 | func (t Token) IsDelimiter() bool { 135 | return delimBegin < t.Type && t.Type < delimEnd 136 | } 137 | 138 | // Pos describes a position in a text file. 139 | type Pos struct { 140 | Line int 141 | Column int 142 | } 143 | 144 | const ( 145 | // Only needed during lexing, no tokens. 146 | eof rune = -1 147 | hyphen rune = '-' 148 | backtick rune = '`' 149 | newline rune = '\n' 150 | slash rune = '/' 151 | asterisk rune = '*' 152 | ) 153 | 154 | var keywordTokenTypes = map[string]TokenType{ 155 | "version": VERSION, 156 | "errors": ERRORS, 157 | "enum": ENUM, 158 | "type": TYPE, 159 | "service": SERVICE, 160 | "call": CALL, 161 | "stream": STREAM, 162 | "async": ASYNC, 163 | "arg": ARG, 164 | "ret": RET, 165 | "maxArgSize": MAXARGSIZE, 166 | "maxRetSize": MAXRETSIZE, 167 | "timeout": TIMEOUT, 168 | "map": MAP, 169 | } 170 | 171 | func toKeywordTokenType(s string) TokenType { 172 | t, ok := keywordTokenTypes[s] 173 | if !ok { 174 | return ILLEGAL 175 | } 176 | return t 177 | } 178 | 179 | var delimTokenTypes = map[rune]TokenType{ 180 | '(': LPAREN, 181 | ')': RPAREN, 182 | '{': LBRACE, 183 | '}': RBRACE, 184 | '[': LBRACK, 185 | ']': RBRACK, 186 | ':': COLON, 187 | '=': EQUAL, 188 | ',': COMMA, 189 | '*': ASTERISK, 190 | } 191 | 192 | func isDelim(r rune) (ok bool) { 193 | _, ok = delimTokenTypes[r] 194 | return 195 | } 196 | 197 | func toDelimTokenType(r rune) TokenType { 198 | t, ok := delimTokenTypes[r] 199 | if !ok { 200 | return ILLEGAL 201 | } 202 | return t 203 | } 204 | -------------------------------------------------------------------------------- /internal/codegen/parser/parser.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package parser 29 | 30 | import ( 31 | "fmt" 32 | "math" 33 | "strconv" 34 | "time" 35 | 36 | "code.cloudfoundry.org/bytefmt" 37 | "github.com/desertbit/orbit/internal/codegen/ast" 38 | "github.com/desertbit/orbit/internal/codegen/lexer" 39 | ) 40 | 41 | type stateFn func(p *parser, f *ast.File) stateFn 42 | 43 | type parser struct { 44 | lx lexer.Lexer 45 | 46 | tk lexer.Token // The current token from the lexer. 47 | unreadTk lexer.Token // A previous current token that has been unread. 48 | } 49 | 50 | // Parse parses the output of the lexer until an EOF or an error is encountered. 51 | // If error is nil, the returned ast.File contains the parsed input. 52 | func Parse(lx lexer.Lexer) (*ast.File, error) { 53 | // Create the parser. 54 | p := &parser{lx: lx} 55 | 56 | // Run the parser. 57 | return p.run() 58 | } 59 | 60 | func (p *parser) expectIdent() (string, error) { 61 | err := p.next() 62 | if err != nil { 63 | return "", err 64 | } else if p.tk.Type != lexer.IDENT { 65 | return "", p.errorf("expected identifier, got %s", p.tk.Value) 66 | } 67 | 68 | return p.tk.Value, nil 69 | } 70 | 71 | func (p *parser) expectInt() (int, error) { 72 | err := p.next() 73 | if err != nil { 74 | return 0, err 75 | } else if p.tk.Type != lexer.INT { 76 | return 0, p.errorf("expected integer, got %s", p.tk.Value) 77 | } 78 | 79 | i, err := strconv.Atoi(p.tk.Value) 80 | if err != nil { 81 | return 0, p.errorf("expected integer, but failed to parse %s, %v", p.tk.Value, err) 82 | } 83 | 84 | return i, nil 85 | } 86 | 87 | func (p *parser) expectDuration() (time.Duration, error) { 88 | err := p.next() 89 | if err != nil { 90 | return 0, err 91 | } else if p.tk.Type != lexer.IDENT { 92 | return 0, p.errorf("expected time duration identifier, got %s", p.tk.Value) 93 | } 94 | 95 | dur, err := time.ParseDuration(p.tk.Value) 96 | if err != nil { 97 | return 0, p.errorf("expected time duration, but failed to parse %s, %v", p.tk.Value, err) 98 | } 99 | 100 | return dur, nil 101 | } 102 | 103 | // Can return -1 as special value. 104 | func (p *parser) expectByteSize() (int64, error) { 105 | err := p.next() 106 | if err != nil { 107 | return 0, err 108 | } else if p.tk.Type == lexer.INT { 109 | // Check for special '-1' value. 110 | if p.tk.Value != "-1" { 111 | return 0, p.errorf("expected byte size, got integer %s", p.tk.Type) 112 | } 113 | 114 | return -1, nil 115 | } else if p.tk.Type != lexer.IDENT { 116 | return 0, p.errorf("expected byte size identifier, got %s", p.tk.Value) 117 | } 118 | 119 | size, err := bytefmt.ToBytes(p.tk.Value) 120 | if err != nil { 121 | return 0, p.errorf("expected byte size, but failed to parse %s, %v", p.tk.Value, err) 122 | } 123 | 124 | // Check, that the size is not larger than uint32. 125 | if size > uint64(math.MaxUint32) { 126 | return 0, p.errorf("byte size too large, max is %d", math.MaxUint32) 127 | } 128 | 129 | return int64(size), nil 130 | } 131 | 132 | func (p *parser) expectToken(dl lexer.TokenType) error { 133 | err := p.next() 134 | if err != nil || p.tk.Type != dl { 135 | return p.errorf("expected symbol %s, got %s", dl.String(), p.tk.Value) 136 | } 137 | 138 | return nil 139 | } 140 | 141 | func (p *parser) checkToken(dl lexer.TokenType) bool { 142 | if p.next() != nil { 143 | return false 144 | } else if p.tk.Type != dl { 145 | p.backup() 146 | return false 147 | } 148 | 149 | return true 150 | } 151 | 152 | func (p *parser) next() error { 153 | // Check, if a token has been unread, which must be consumed first. 154 | if p.unreadTk != (lexer.Token{}) { 155 | p.tk = p.unreadTk 156 | p.unreadTk = lexer.Token{} 157 | return nil 158 | } 159 | 160 | p.tk = p.lx.Next() 161 | if p.tk.Type == lexer.ILLEGAL { 162 | // Backup the illegal token so every p.next call returns it again. 163 | p.backup() 164 | return p.errorf(p.tk.Value) 165 | } 166 | 167 | return nil 168 | } 169 | 170 | func (p *parser) backup() { 171 | p.unreadTk = p.tk 172 | } 173 | 174 | func (p *parser) errorf(format string, args ...interface{}) error { 175 | format += " (pos %d:%d)" 176 | args = append(args, p.tk.Pos.Line, p.tk.Pos.Column) 177 | return fmt.Errorf(format, args...) 178 | } 179 | -------------------------------------------------------------------------------- /internal/codegen/parser/parser_datatype.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package parser 29 | 30 | import ( 31 | "github.com/desertbit/orbit/internal/codegen/ast" 32 | "github.com/desertbit/orbit/internal/codegen/lexer" 33 | ) 34 | 35 | func (p *parser) expectTypeDefinition() ([]*ast.TypeField, error) { 36 | // Orbit file example: 37 | /* 38 | 39 | name string `validate:"required,min=1"` 40 | age int `validate:"required,min=1,max=155"` 41 | locale *string `validate:"required,len=5"` 42 | address string `validate:"omitempty"` 43 | } 44 | */ 45 | 46 | // Type Fields. 47 | var tfs []*ast.TypeField 48 | for !p.checkToken(lexer.RBRACE) { 49 | tf := &ast.TypeField{Pos: p.tk.Pos} 50 | 51 | // Identifier. 52 | var err error 53 | tf.Name, err = p.expectIdent() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | // Data type. 59 | tf.DataType, err = p.expectDataType() 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | // Optional struct tag. 65 | if p.checkToken(lexer.RAWSTRING) { 66 | tf.StructTag = p.tk.Value 67 | } 68 | 69 | // Add type field to type. 70 | tfs = append(tfs, tf) 71 | } 72 | 73 | return tfs, nil 74 | } 75 | 76 | func (p *parser) expectDataType() (ast.DataType, error) { 77 | pointer := p.checkToken(lexer.ASTERISK) 78 | 79 | if p.checkToken(lexer.LBRACK) && p.checkToken(lexer.RBRACK) { 80 | return p.expectArrType(pointer) 81 | } else if p.checkToken(lexer.MAP) { 82 | return p.expectMapType(pointer) 83 | } else { 84 | return p.expectAnyType(pointer) 85 | } 86 | } 87 | 88 | func (p *parser) expectMapType(pointer bool) (*ast.MapType, error) { 89 | // '['. 90 | err := p.expectToken(lexer.LBRACK) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | // Key type. 96 | keyIsPointer := p.checkToken(lexer.ASTERISK) 97 | key, err := p.expectAnyType(keyIsPointer) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | // ']'. 103 | err = p.expectToken(lexer.RBRACK) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | // Value type. 109 | value, err := p.expectDataType() 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | return ast.NewMapType(key, value, p.tk.Pos, pointer), nil 115 | } 116 | 117 | func (p *parser) expectArrType(pointer bool) (*ast.ArrType, error) { 118 | // Expect any type. 119 | elem, err := p.expectDataType() 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | return ast.NewArrType(elem, p.tk.Pos, pointer), nil 125 | } 126 | 127 | func (p *parser) expectAnyType(pointer bool) (*ast.AnyType, error) { 128 | // Identifier. 129 | name, err := p.expectIdent() 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | // Ensure private name is lowercase. 135 | return ast.NewAnyType(name, p.tk.Pos, pointer), nil 136 | } 137 | -------------------------------------------------------------------------------- /internal/codegen/parser/testdata/valid.orbit: -------------------------------------------------------------------------------- 1 | version 1 2 | 3 | service { 4 | call c1 { 5 | arg: { 6 | id int `json:"ID" yaml:"id"` 7 | } 8 | ret: { 9 | sum float32 10 | } 11 | errors: theFirstError 12 | } 13 | call c2 { 14 | async 15 | arg: { 16 | ts time 17 | } 18 | ret: { 19 | data []map[string][]Ret 20 | } 21 | timeout: 1m 22 | maxArgSize: 154KB 23 | maxRetSize: 5MiB 24 | errors: theFirstError, theThirdError 25 | } 26 | call c3 {} 27 | 28 | call rc1 { 29 | arg: Arg 30 | ret: { 31 | s string 32 | i *int 33 | m *map[string]int 34 | sl []time 35 | st Ret 36 | crazy map[string][][]map[string]En1 37 | } 38 | } 39 | call rc2 { 40 | async 41 | arg: { 42 | f float64 43 | b byte 44 | u8 uint8 45 | u16 uint16 46 | u32 uint32 47 | u64 *uint64 48 | } 49 | } 50 | call rc3 {} 51 | 52 | stream s1 {} 53 | stream s2 { 54 | arg: { 55 | id string `validator:"required"` 56 | } 57 | } 58 | stream s3 { 59 | ret: Ret 60 | } 61 | 62 | stream rs1 { 63 | arg: Arg 64 | ret: Ret 65 | } 66 | stream rs2 {} 67 | } 68 | 69 | type Arg { 70 | s string `json:"STRING"` 71 | i int 72 | m map[string]*int 73 | sl []time 74 | dur duration 75 | st Ret 76 | stp *Ret 77 | crazy map[string][][]map[string]En1 78 | } 79 | 80 | type Ret { 81 | f float64 82 | b byte 83 | u8 uint8 84 | u16 uint16 85 | u32 uint32 86 | u64 uint64 87 | } 88 | 89 | enum En1 { 90 | Val1 = 1 91 | Val2 = 2 92 | Val3 = 3 93 | } 94 | 95 | errors { 96 | theFirstError = 1 97 | theSecondError = 2 98 | theThirdError = 3 99 | } 100 | 101 | errors {} -------------------------------------------------------------------------------- /internal/codegen/validate/validate.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "github.com/desertbit/orbit/internal/codegen/ast" 5 | ) 6 | 7 | // Validate validates the given ast.File for various sanity checks and attempts to resolve 8 | // all AnyTypes to either a Struct or Enum Type. 9 | func Validate(f *ast.File) error { 10 | // Service. 11 | err := validateService(f) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | // Types. 17 | for i, t := range f.Types { 18 | for j := i + 1; j < len(f.Types); j++ { 19 | // Check for duplicate names. 20 | if t.Name == f.Types[j].Name { 21 | return ast.NewErr(t.Line, "type '%s' declared twice", t.Name) 22 | } 23 | } 24 | 25 | for j, tf := range t.Fields { 26 | for k := j + 1; k < len(t.Fields); k++ { 27 | // Check for duplicate field names. 28 | if tf.Name == t.Fields[k].Name { 29 | return ast.NewErr( 30 | tf.Line, "field '%s' of type '%s' declared twice", tf.Name, t.Name, 31 | ) 32 | } 33 | } 34 | 35 | // Resolve all AnyTypes. 36 | tf.DataType, err = resolveAnyType(tf.DataType, f) 37 | if err != nil { 38 | return err 39 | } 40 | } 41 | } 42 | 43 | // Errors. 44 | for i, e := range f.Errs { 45 | for j := i + 1; j < len(f.Errs); j++ { 46 | e2 := f.Errs[j] 47 | 48 | // Check for duplicate names. 49 | if e.Name == e2.Name { 50 | return ast.NewErr(e.Line, "error '%s' declared twice", e.Name) 51 | } 52 | 53 | // Check for valid id. 54 | if e.ID <= 0 { 55 | return ast.NewErr(e.Line, "invalid error id, must be greater than 0") 56 | } 57 | 58 | // Check for duplicate id. 59 | if e.ID == e2.ID { 60 | return ast.NewErr(e.Line, "error '%s' has same id as '%s'", e.Name, e2.Name) 61 | } 62 | } 63 | } 64 | 65 | // Enums. 66 | for i, en := range f.Enums { 67 | for j := i + 1; j < len(f.Enums); j++ { 68 | // Check for duplicate name. 69 | if en.Name == f.Enums[j].Name { 70 | return ast.NewErr(en.Line, "enum '%s' declared twice", en.Name) 71 | } 72 | } 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func resolveAnyType(dt ast.DataType, f *ast.File) (ast.DataType, error) { 79 | var err error 80 | 81 | switch v := dt.(type) { 82 | case *ast.AnyType: 83 | // Resolve the type. 84 | for _, t := range f.Types { 85 | if t.Name == v.ID() { 86 | return ast.NewStructType(v.ID(), v.Pos(), v.Pointer()), nil 87 | } 88 | } 89 | for _, en := range f.Enums { 90 | if en.Name == v.ID() { 91 | return ast.NewEnumType(v.ID(), v.Pos(), v.Pointer()), nil 92 | } 93 | } 94 | switch v.ID() { 95 | case ast.TypeByte, ast.TypeString, ast.TypeTime, ast.TypeDuration, 96 | ast.TypeBool, 97 | ast.TypeInt, ast.TypeInt8, ast.TypeInt16, ast.TypeInt32, ast.TypeInt64, 98 | ast.TypeUInt, ast.TypeUInt8, ast.TypeUInt16, ast.TypeUInt32, ast.TypeUInt64, 99 | ast.TypeFloat32, ast.TypeFloat64: 100 | return ast.NewBaseType(v.ID(), v.Pos(), v.Pointer()), nil 101 | } 102 | 103 | return nil, ast.NewErr(v.Pos().Line, "could not resolve unknown type '%s'", v.ID()) 104 | case *ast.MapType: 105 | v.Key, err = resolveAnyType(v.Key, f) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | // Key types can only be base or enum types. 111 | switch v.Key.(type) { 112 | case *ast.BaseType, *ast.EnumType: 113 | default: 114 | return nil, ast.NewErr(v.Pos().Line, "invalid map key type '%s'", v.Key.ID()) 115 | } 116 | 117 | v.Value, err = resolveAnyType(v.Value, f) 118 | if err != nil { 119 | return nil, err 120 | } 121 | case *ast.ArrType: 122 | v.Elem, err = resolveAnyType(v.Elem, f) 123 | if err != nil { 124 | return nil, err 125 | } 126 | } 127 | 128 | // No change. 129 | return dt, nil 130 | } 131 | -------------------------------------------------------------------------------- /internal/codegen/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package codegen 29 | 30 | const ( 31 | // The version of the .orbit syntax this codegen supports. 32 | // Hint: Increasing this to the next version renders the previous 33 | // version unusable with this codegen. Should only be done, if the 34 | // syntax changes in a way that is not backwards compatible. 35 | OrbitFileVersion = 1 36 | 37 | // The internal version of the codegen used to cache which 38 | // .orbit files must be generated. 39 | // Hint: Increasing this to the next version indicates that the 40 | // codegen has been improved, but no backwards incompatible changes 41 | // have been introduced. May be used to invalidate the build cache 42 | // of the codegen, so that a project definitely uses its new features. 43 | CacheVersion = 12 44 | ) 45 | -------------------------------------------------------------------------------- /internal/rpc/rpc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package rpc 29 | 30 | import ( 31 | "errors" 32 | "fmt" 33 | "net" 34 | 35 | "github.com/desertbit/orbit/internal/api" 36 | "github.com/desertbit/orbit/pkg/packet" 37 | ) 38 | 39 | // Read the rpc request from the connection, 40 | // If a payload or header buffer is passed, then this buffer will be used 41 | // if the capacity is sufficient. 42 | func Read( 43 | conn net.Conn, 44 | headerBuffer []byte, 45 | payloadBuffer []byte, 46 | maxHeaderSize int, 47 | maxPayloadSize int, 48 | ) ( 49 | reqType api.RPCType, 50 | header []byte, 51 | payload []byte, 52 | err error, 53 | ) { 54 | var ( 55 | bytesRead, n int 56 | reqTypeBuf = make([]byte, 1) 57 | ) 58 | 59 | // Read the reqType from the stream. 60 | for bytesRead == 0 { 61 | n, err = conn.Read(reqTypeBuf) 62 | if err != nil { 63 | return 64 | } 65 | bytesRead += n 66 | } 67 | 68 | reqType = api.RPCType(reqTypeBuf[0]) 69 | 70 | // Read the header from the stream. 71 | header, err = packet.Read(conn, headerBuffer, maxHeaderSize) 72 | if err != nil { 73 | err = fmt.Errorf("failed to read header: %w", err) 74 | return 75 | } 76 | 77 | // Read the optional payload from the stream. 78 | payload, err = packet.Read(conn, payloadBuffer, maxPayloadSize) 79 | if err != nil { 80 | if errors.Is(err, packet.ErrZeroData) { 81 | err = nil 82 | } else { 83 | err = fmt.Errorf("failed to read payload: %w", err) 84 | return 85 | } 86 | } 87 | 88 | return 89 | } 90 | 91 | // Write the rpc request to the connection. 92 | func Write( 93 | conn net.Conn, 94 | reqType api.RPCType, 95 | header []byte, 96 | payload []byte, 97 | maxHeaderSize int, 98 | maxPayloadSize int, 99 | ) (err error) { 100 | // Write the request type. 101 | _, err = conn.Write([]byte{byte(reqType)}) 102 | if err != nil { 103 | return 104 | } 105 | 106 | // Write the header. 107 | err = packet.Write(conn, header, maxHeaderSize) 108 | if err != nil { 109 | err = fmt.Errorf("failed to write header: %w", err) 110 | return 111 | } 112 | 113 | // Write the payload. 114 | err = packet.Write(conn, payload, maxPayloadSize) 115 | if err != nil { 116 | err = fmt.Errorf("failed to write payload: %w", err) 117 | return 118 | } 119 | return 120 | } 121 | -------------------------------------------------------------------------------- /internal/strutil/strutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | /* 29 | Package strutil is the common sin of every Go programmer. 30 | It contains utility functions around strings. 31 | */ 32 | package strutil 33 | 34 | import ( 35 | "crypto/rand" 36 | "unicode" 37 | ) 38 | 39 | const ( 40 | // randStrChars defines the possible characters for the RandomString() function. 41 | // RandomString() expects only ASCII characters, therefore, no char spanning 42 | // more than 1 byte in UTF-8 encoding must be used. 43 | randStrChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 44 | // randStrCharsLen stores the length of randStrChars. 45 | randStrCharsLen = byte(len(randStrChars)) 46 | ) 47 | 48 | // RandomString generates a random string with len n using the crypto/rand RNG. 49 | // The returned string contains only chars defined in the randStrChars constant. 50 | func RandomString(n uint) (string, error) { 51 | bytes := make([]byte, n) 52 | _, err := rand.Read(bytes) 53 | if err != nil { 54 | return "", err 55 | } 56 | 57 | for i, b := range bytes { 58 | bytes[i] = randStrChars[b%randStrCharsLen] 59 | } 60 | return string(bytes), nil 61 | } 62 | 63 | // FirstUpper returns a copy of s, where the first rune is 64 | // guaranteed to be an uppercase letter, like unicode.ToUpper 65 | // suggests. 66 | func FirstUpper(s string) string { 67 | for i, v := range s { 68 | return string(unicode.ToUpper(v)) + s[i+1:] 69 | } 70 | return "" 71 | } 72 | 73 | // FirstLower returns a copy of s, where the first rune is 74 | // guaranteed to be a lowercase letter, like unicode.ToLower 75 | // suggests. 76 | func FirstLower(s string) string { 77 | for i, v := range s { 78 | return string(unicode.ToLower(v)) + s[i+1:] 79 | } 80 | return "" 81 | } 82 | -------------------------------------------------------------------------------- /internal/strutil/strutil_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package strutil_test 29 | 30 | import ( 31 | "testing" 32 | 33 | "github.com/desertbit/orbit/internal/strutil" 34 | "github.com/stretchr/testify/require" 35 | ) 36 | 37 | func TestRandomString(t *testing.T) { 38 | t.Parallel() 39 | 40 | testCases := []uint{0, 1, 10, 100} 41 | 42 | for i, c := range testCases { 43 | s, err := strutil.RandomString(c) 44 | require.NoErrorf(t, err, "case %d", i) 45 | require.Lenf(t, s, int(c), "case %d", i) 46 | } 47 | } 48 | 49 | var benchRandomStringRes string 50 | 51 | func BenchmarkRandomString(b *testing.B) { 52 | var ( 53 | res string 54 | err error 55 | ) 56 | for i := 0; i < b.N; i++ { 57 | res, err = strutil.RandomString(32) 58 | if err != nil { 59 | b.Fatal(err) 60 | } 61 | } 62 | benchRandomStringRes = res 63 | } 64 | 65 | func TestFirstUpper(t *testing.T) { 66 | t.Parallel() 67 | 68 | testCases := []struct { 69 | val string 70 | exp string 71 | }{ 72 | {val: "", exp: ""}, // 0 73 | {val: "hello", exp: "Hello"}, 74 | {val: "Hello", exp: "Hello"}, 75 | {val: "HELLO", exp: "HELLO"}, 76 | {val: "hELLO", exp: "HELLO"}, 77 | {val: " Hello", exp: " Hello"}, // 5 78 | {val: "He llo", exp: "He llo"}, 79 | } 80 | 81 | for i, c := range testCases { 82 | require.Exactly(t, c.exp, strutil.FirstUpper(c.val), "case %d", i) 83 | } 84 | } 85 | 86 | func TestFirstLower(t *testing.T) { 87 | t.Parallel() 88 | 89 | testCases := []struct { 90 | val string 91 | exp string 92 | }{ 93 | {val: "", exp: ""}, // 0 94 | {val: "hello", exp: "hello"}, 95 | {val: "Hello", exp: "hello"}, 96 | {val: "HELLO", exp: "hELLO"}, 97 | {val: " Hello", exp: " Hello"}, 98 | {val: "He llo", exp: "he llo"}, // 5 99 | } 100 | 101 | for i, c := range testCases { 102 | require.Exactly(t, c.exp, strutil.FirstLower(c.val), "case %d", i) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /internal/throttler/throttler.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package throttler 29 | 30 | import ( 31 | "time" 32 | ) 33 | 34 | type Throttler struct { 35 | duration time.Duration 36 | sub time.Duration 37 | lastTimestamp time.Time 38 | } 39 | 40 | func New(duration time.Duration) *Throttler { 41 | return &Throttler{ 42 | duration: duration, 43 | } 44 | } 45 | 46 | func (t *Throttler) Throttle(now time.Time) (isThrottling bool) { 47 | t.sub = now.Sub(t.lastTimestamp) 48 | if t.sub < t.duration { 49 | return true 50 | } 51 | t.lastTimestamp = now 52 | return false 53 | } 54 | 55 | func (t *Throttler) ThrottleSleep(now time.Time) (isThrottling bool) { 56 | t.sub = now.Sub(t.lastTimestamp) 57 | if t.sub < t.duration { 58 | time.Sleep(t.duration - t.sub) 59 | return true 60 | } 61 | t.lastTimestamp = now 62 | return false 63 | } 64 | -------------------------------------------------------------------------------- /pkg/client/chain.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package client 29 | 30 | import ( 31 | "sync" 32 | ) 33 | 34 | type chainChan chan chainData 35 | 36 | type chainData struct { 37 | Data []byte 38 | Err error 39 | } 40 | 41 | type chain struct { 42 | mutex sync.Mutex 43 | chanMap map[uint32]chainChan 44 | key uint32 45 | } 46 | 47 | func newChain() *chain { 48 | return &chain{ 49 | chanMap: make(map[uint32]chainChan), 50 | } 51 | } 52 | 53 | func (c *chain) New() (key uint32, cc chainChan) { 54 | cc = make(chainChan, 1) 55 | 56 | c.mutex.Lock() 57 | defer c.mutex.Unlock() 58 | 59 | c.incrementKey() 60 | 61 | key = c.key 62 | c.chanMap[key] = cc 63 | return 64 | } 65 | 66 | func (c *chain) NewKey() (key uint32) { 67 | c.mutex.Lock() 68 | defer c.mutex.Unlock() 69 | 70 | c.incrementKey() 71 | return c.key 72 | } 73 | 74 | // Get returns the channel with the given key. 75 | // Returns nil, if not found. 76 | func (c *chain) Get(key uint32) (cc chainChan) { 77 | c.mutex.Lock() 78 | cc = c.chanMap[key] 79 | c.mutex.Unlock() 80 | return 81 | } 82 | 83 | // Delete the channel with the given key from the chain. 84 | // If the key does not exist, this is a no-op. 85 | func (c *chain) Delete(key uint32) { 86 | c.mutex.Lock() 87 | delete(c.chanMap, key) 88 | c.mutex.Unlock() 89 | } 90 | 91 | func (c *chain) incrementKey() { 92 | // Create next key. 93 | c.key++ 94 | 95 | // We avoid a key of 0, as this is a special value. 96 | if c.key == 0 { 97 | c.key++ 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /pkg/client/client_session.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package client 29 | 30 | import ( 31 | "context" 32 | "fmt" 33 | "time" 34 | 35 | "github.com/desertbit/orbit/internal/throttler" 36 | ) 37 | 38 | func (c *client) updateState(s State) { 39 | // Do not block, drain old values if full and retry. 40 | select { 41 | case c.stateChan <- s: 42 | default: 43 | select { 44 | case <-c.stateChan: 45 | default: 46 | } 47 | select { 48 | case c.stateChan <- s: 49 | default: 50 | } 51 | } 52 | } 53 | 54 | func (c *client) getSession() (s *session) { 55 | c.sessionMx.Lock() 56 | s = c.session 57 | c.sessionMx.Unlock() 58 | return 59 | } 60 | 61 | func (c *client) setSession(s *session) { 62 | c.sessionMx.Lock() 63 | c.session = s 64 | c.sessionMx.Unlock() 65 | } 66 | 67 | // connectedSession returns the connected session or triggers a connect request. 68 | // Fails if no connection could be established. 69 | // Always returns a connected session if err is nil. 70 | func (c *client) connectedSession(ctx context.Context) (s *session, err error) { 71 | if c.IsClosing() { 72 | err = ErrClosed 73 | return 74 | } 75 | 76 | s = c.getSession() 77 | if s != nil { 78 | return 79 | } 80 | 81 | var ( 82 | closingChan = c.ClosingChan() 83 | retChan = make(chan interface{}, 1) 84 | ctxDone = ctx.Done() 85 | ) 86 | 87 | select { 88 | case <-closingChan: 89 | err = ErrClosed 90 | return 91 | case <-ctxDone: 92 | err = ctx.Err() 93 | return 94 | case c.connectSessionChan <- retChan: 95 | } 96 | 97 | select { 98 | case <-closingChan: 99 | err = ErrClosed 100 | return 101 | case <-ctxDone: 102 | err = ctx.Err() 103 | return 104 | case r := <-retChan: 105 | switch v := r.(type) { 106 | case *session: 107 | s = v 108 | case error: 109 | err = v 110 | default: 111 | err = fmt.Errorf("invalid connected session return value") 112 | } 113 | return 114 | } 115 | } 116 | 117 | func (c *client) startSessionRoutine() { 118 | go c.sessionRoutine() 119 | } 120 | 121 | func (c *client) sessionRoutine() { 122 | defer c.Close_() 123 | 124 | var ( 125 | isReconnect bool 126 | 127 | closingChan = c.ClosingChan() 128 | connectThrottler = throttler.New(c.opts.ConnectThrottleDuration) 129 | ) 130 | 131 | Loop: 132 | for { 133 | select { 134 | case <-closingChan: 135 | return 136 | 137 | case r := <-c.connectSessionChan: 138 | // Throttle between connection attempts. 139 | connectThrottler.ThrottleSleep(time.Now()) 140 | 141 | // Set the new state. 142 | if isReconnect { 143 | c.updateState(StateReconnecting) 144 | } else { 145 | c.updateState(StateConnecting) 146 | } 147 | 148 | // Try to connect to session. 149 | s, err := connectSession(c, c.opts) 150 | if err != nil { 151 | c.updateState(StateDisconnected) 152 | r <- fmt.Errorf("%w: %v", ErrConnect, err) // Notify. 153 | continue Loop 154 | } 155 | 156 | // Publish the newly connected session. 157 | c.setSession(s) 158 | 159 | // Set the new state. 160 | if isReconnect { 161 | c.updateState(StateReconnected) 162 | } else { 163 | c.updateState(StateConnected) 164 | } 165 | isReconnect = true 166 | 167 | // Notify. 168 | r <- s 169 | 170 | // Wait for disconnection. 171 | // Just handle the connectSessionChan and keep it drained. 172 | sessionClosingChan := s.ClosingChan() 173 | SubLoop: 174 | for { 175 | select { 176 | case <-closingChan: 177 | s.Close() 178 | c.updateState(StateDisconnected) 179 | return 180 | case <-sessionClosingChan: 181 | break SubLoop 182 | case r := <-c.connectSessionChan: 183 | r <- s 184 | } 185 | } 186 | 187 | // Reset the session again. 188 | c.setSession(nil) 189 | 190 | // Set the new state. 191 | c.updateState(StateDisconnected) 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /pkg/client/context.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package client 29 | 30 | import "context" 31 | 32 | // A Context defines the client context which extends the context.Context interface. 33 | type Context interface { 34 | context.Context 35 | 36 | // SetContext can be used to wrap the context.Context with additonal deadlines, ... 37 | SetContext(ctx context.Context) 38 | 39 | // Session returns the current active session. 40 | Session() Session 41 | 42 | // SetRaw sets the raw header byte slice defined by the key. 43 | // This data is send to the service. 44 | SetHeader(key string, data []byte) 45 | 46 | // Data returns the value defined by the key. Returns nil if not present. 47 | Data(key string) interface{} 48 | 49 | // SetData sets the value defined by the key. 50 | SetData(key string, v interface{}) 51 | } 52 | 53 | type clientContext struct { 54 | context.Context 55 | 56 | s Session 57 | header map[string][]byte 58 | data map[string]interface{} 59 | } 60 | 61 | func newContext(ctx context.Context, s Session) *clientContext { 62 | return &clientContext{ 63 | Context: ctx, 64 | s: s, 65 | header: make(map[string][]byte), 66 | } 67 | } 68 | 69 | func (c *clientContext) SetContext(ctx context.Context) { 70 | c.Context = ctx 71 | } 72 | 73 | func (c *clientContext) Session() Session { 74 | return c.s 75 | } 76 | 77 | func (c *clientContext) SetHeader(key string, data []byte) { 78 | c.header[key] = data 79 | } 80 | 81 | func (c *clientContext) Data(key string) interface{} { 82 | if c.data == nil { 83 | return nil 84 | } 85 | return c.data[key] 86 | } 87 | 88 | func (c *clientContext) SetData(key string, v interface{}) { 89 | if c.data == nil { 90 | c.data = make(map[string]interface{}) 91 | } 92 | c.data[key] = v 93 | } 94 | -------------------------------------------------------------------------------- /pkg/client/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package client 29 | 30 | import "errors" 31 | 32 | var ( 33 | ErrClosed = errors.New("closed") 34 | ErrNoData = errors.New("no data available") 35 | ErrConnect = errors.New("connect failed") 36 | ErrInvalidVersion = errors.New("invalid version") 37 | ErrCatchedPanic = errors.New("catched panic") 38 | ) 39 | 40 | // The Error type extends the standard go error by a simple 41 | // integer code. It is returned in the Call- functions of this 42 | // package and allows callers that use them to check for common 43 | // errors via the code. 44 | type Error interface { 45 | // Embeds the standard go error interface. 46 | error 47 | 48 | // Code returns an integer that can give a hint about the 49 | // type of error that occurred. 50 | Code() int 51 | } 52 | 53 | // NewError returns a new error with the given message and code. 54 | func NewError(code int, msg string) Error { 55 | return errImpl{ 56 | msg: msg, 57 | code: code, 58 | } 59 | } 60 | 61 | // Implements the Error interface. 62 | type errImpl struct { 63 | msg string 64 | code int 65 | } 66 | 67 | func (e errImpl) Error() string { 68 | return e.msg 69 | } 70 | 71 | func (e errImpl) Code() int { 72 | return e.code 73 | } 74 | -------------------------------------------------------------------------------- /pkg/client/hook.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package client 29 | 30 | import ( 31 | "github.com/desertbit/orbit/pkg/transport" 32 | ) 33 | 34 | type Hooks []Hook 35 | 36 | // Hook allows third-party code to hook into orbit's logic, to implement for example 37 | // logging or authentication functionality. 38 | type Hook interface { 39 | // Close is called if the client closes. 40 | Close() error 41 | 42 | // OnSession is called if a new client session is connected to the service. 43 | // RPC and stream routines are handled after this hook. 44 | // Do not use the stream after returning from this hook. 45 | // Return an error to close the session and abort the initialization process. 46 | OnSession(s Session, stream transport.Stream) error 47 | 48 | // OnSessionClosed is called as soon as the session closes. 49 | OnSessionClosed(s Session) 50 | 51 | // OnCall is called before a call request. 52 | // Return an error to abort the call. 53 | OnCall(ctx Context, id string, callKey uint32) error 54 | 55 | // OnCallDone is called after a call request. 56 | // The context is the same as from the OnCall hook. 57 | // If err == nil, then the call completed successfully. 58 | OnCallDone(ctx Context, id string, callKey uint32, err error) 59 | 60 | // OnCallCanceled is called, if a call is canceled. 61 | // The context is the same as from the OnCall hook. 62 | OnCallCanceled(ctx Context, id string, callKey uint32) 63 | 64 | // OnStream is called during a new stream setup. 65 | // Return an error to abort the stream setup. 66 | OnStream(ctx Context, id string) error 67 | 68 | // OnStreamClosed is called after a stream closes. 69 | // The context is the same as from the OnStream hook. 70 | OnStreamClosed(ctx Context, id string) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/client/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package client 29 | 30 | import ( 31 | "errors" 32 | "os" 33 | "time" 34 | 35 | "github.com/desertbit/closer/v3" 36 | "github.com/desertbit/orbit/pkg/codec" 37 | "github.com/desertbit/orbit/pkg/codec/msgpack" 38 | "github.com/desertbit/orbit/pkg/transport" 39 | "github.com/rs/zerolog" 40 | ) 41 | 42 | const ( 43 | defaultCallTimeout = 30 * time.Second 44 | defaultConnectTimeout = 10 * time.Second 45 | defaultConnectThrottleDuration = 2 * time.Second 46 | defaultHandshakeTimeout = 7 * time.Second 47 | defaultStreamInitTimeout = 10 * time.Second 48 | 49 | defaultMaxArgSize = 4 * 1024 * 1024 // 4 MB 50 | defaultMaxRetSize = 4 * 1024 * 1024 // 4 MB 51 | defaultMaxHeaderSize = 500 * 1024 // 500 KB 52 | ) 53 | 54 | type Options struct { 55 | // Host specifies the destination host address. This value must be set. 56 | Host string 57 | 58 | // Transport specifies the communication backend. This value must be set. 59 | Transport transport.Transport 60 | 61 | // Optional values: 62 | // ################ 63 | 64 | // Closer defines the closer instance. A default closer will be created if unspecified. 65 | Closer closer.Closer 66 | 67 | // Codec defines the transport encoding. A default codec will be used if unspecified. 68 | Codec codec.Codec 69 | 70 | // Hooks specifies the hooks executed during certain actions. The order of the hooks is stricly followed. 71 | Hooks Hooks 72 | 73 | // Log specifies the default logger backend. A default logger will be used if unspecified. 74 | Log *zerolog.Logger 75 | 76 | // CallTimeout specifies the default timeout for each call. 77 | // Set to -1 for no timeout. 78 | CallTimeout time.Duration 79 | 80 | // ConnectTimeout specifies the timeout duration after a service connect attempt. 81 | ConnectTimeout time.Duration 82 | 83 | // ConnectThrottleDuration specifies the wait duration between subsequent connection attempts. 84 | ConnectThrottleDuration time.Duration 85 | 86 | // HandshakeTimeout specifies the connection initialization timeout. 87 | HandshakeTimeout time.Duration 88 | 89 | // StreamInitTimeout specifies the default timeout for a stream setup. 90 | StreamInitTimeout time.Duration 91 | 92 | // PrintPanicStackTraces prints stack traces of catched panics. 93 | PrintPanicStackTraces bool 94 | 95 | // MaxArgSize defines the default maximum argument payload size for RPC calls. 96 | MaxArgSize int 97 | 98 | // MaxRetSize defines the default maximum return payload size for RPC calls. 99 | MaxRetSize int 100 | 101 | // MaxHeaderSize defines the maximum header size for calls and streams. 102 | MaxHeaderSize int 103 | } 104 | 105 | func (o *Options) setDefaults() { 106 | if o.Closer == nil { 107 | o.Closer = closer.New() 108 | } 109 | if o.Codec == nil { 110 | o.Codec = msgpack.Codec 111 | } 112 | if o.Log == nil { 113 | l := zerolog.New(zerolog.ConsoleWriter{ 114 | Out: os.Stderr, 115 | TimeFormat: time.RFC3339, 116 | }).With().Timestamp().Str("component", "orbit").Logger() 117 | o.Log = &l 118 | } 119 | if o.CallTimeout == 0 { 120 | o.CallTimeout = defaultCallTimeout 121 | } 122 | if o.ConnectTimeout == 0 { 123 | o.ConnectTimeout = defaultConnectTimeout 124 | } 125 | if o.ConnectThrottleDuration == 0 { 126 | o.ConnectThrottleDuration = defaultConnectThrottleDuration 127 | } 128 | if o.HandshakeTimeout == 0 { 129 | o.HandshakeTimeout = defaultHandshakeTimeout 130 | } 131 | if o.StreamInitTimeout == 0 { 132 | o.StreamInitTimeout = defaultStreamInitTimeout 133 | } 134 | if o.MaxArgSize == 0 { 135 | o.MaxArgSize = defaultMaxArgSize 136 | } 137 | if o.MaxRetSize == 0 { 138 | o.MaxRetSize = defaultMaxRetSize 139 | } 140 | if o.MaxHeaderSize == 0 { 141 | o.MaxHeaderSize = defaultMaxHeaderSize 142 | } 143 | } 144 | 145 | func (o *Options) validate() error { 146 | if o.Host == "" { 147 | return errors.New("empty host") 148 | } else if o.Transport == nil { 149 | return errors.New("no transport set") 150 | } 151 | return nil 152 | } 153 | -------------------------------------------------------------------------------- /pkg/client/session.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package client 29 | 30 | import ( 31 | "context" 32 | "fmt" 33 | "net" 34 | "sync" 35 | "time" 36 | 37 | "github.com/desertbit/closer/v3" 38 | "github.com/desertbit/orbit/internal/api" 39 | "github.com/desertbit/orbit/pkg/codec" 40 | "github.com/desertbit/orbit/pkg/packet" 41 | "github.com/desertbit/orbit/pkg/transport" 42 | "github.com/rs/zerolog" 43 | ) 44 | 45 | const ( 46 | sessionHandshakeMaxPayloadSize = 1024 // 1 KB 47 | ) 48 | 49 | type Session interface { 50 | closer.Closer 51 | 52 | ID() string 53 | LocalAddr() net.Addr 54 | RemoteAddr() net.Addr 55 | } 56 | 57 | type session struct { 58 | closer.Closer 59 | 60 | id string 61 | conn transport.Conn 62 | handler clientHandler 63 | log *zerolog.Logger 64 | codec codec.Codec 65 | chain *chain 66 | 67 | maxArgSize int 68 | maxRetSize int 69 | maxHeaderSize int 70 | 71 | streamReadMx sync.Mutex 72 | streamWriteMx sync.Mutex 73 | stream transport.Stream 74 | 75 | cancelStreamMx sync.Mutex 76 | cancelStream transport.Stream 77 | } 78 | 79 | // Implements the Session interface. 80 | func (s *session) ID() string { 81 | return s.id 82 | } 83 | 84 | // Implements the Session interface. 85 | func (s *session) LocalAddr() net.Addr { 86 | return s.conn.LocalAddr() 87 | } 88 | 89 | // Implements the Session interface. 90 | func (s *session) RemoteAddr() net.Addr { 91 | return s.conn.RemoteAddr() 92 | } 93 | 94 | func connectSession(h clientHandler, opts *Options) (s *session, err error) { 95 | ctxConnect, cancelConnect := context.WithTimeout(context.Background(), opts.ConnectTimeout) 96 | defer cancelConnect() 97 | 98 | // Connect to the service. 99 | conn, err := opts.Transport.Dial(opts.Closer.CloserOneWay(), ctxConnect, opts.Host) 100 | if err != nil { 101 | return 102 | } 103 | 104 | // Always close the conn on error. 105 | defer func() { 106 | if err != nil { 107 | conn.Close_() 108 | } 109 | }() 110 | 111 | // Create new timeout context. 112 | ctx, cancel := context.WithTimeout(context.Background(), opts.HandshakeTimeout) 113 | defer cancel() 114 | 115 | // Open the stream to the server. 116 | stream, err := conn.OpenStream(ctx) 117 | if err != nil { 118 | return 119 | } 120 | 121 | // Deadline is certainly available. 122 | deadline, _ := ctx.Deadline() 123 | 124 | // Set the deadline for the handshake. 125 | err = stream.SetDeadline(deadline) 126 | if err != nil { 127 | return 128 | } 129 | 130 | // Send the arguments for the handshake to the server. 131 | err = packet.WriteEncode(stream, &api.HandshakeArgs{Version: api.Version}, api.Codec, sessionHandshakeMaxPayloadSize) 132 | if err != nil { 133 | return 134 | } 135 | 136 | // Wait for the server's handshake response. 137 | var ret api.HandshakeRet 138 | err = packet.ReadDecode(stream, &ret, api.Codec, sessionHandshakeMaxPayloadSize) 139 | if err != nil { 140 | return 141 | } else if ret.Code == api.HSInvalidVersion { 142 | return nil, ErrInvalidVersion 143 | } else if ret.Code != api.HSOk { 144 | return nil, fmt.Errorf("unknown handshake code %d", ret.Code) 145 | } 146 | 147 | // Reset the deadline. 148 | err = stream.SetDeadline(time.Time{}) 149 | if err != nil { 150 | return 151 | } 152 | 153 | // Finally, create the orbit session. 154 | s = &session{ 155 | Closer: conn, 156 | 157 | id: ret.SessionID, 158 | conn: conn, 159 | handler: h, 160 | log: opts.Log, 161 | codec: opts.Codec, 162 | chain: newChain(), 163 | 164 | maxArgSize: opts.MaxArgSize, 165 | maxRetSize: opts.MaxRetSize, 166 | maxHeaderSize: opts.MaxHeaderSize, 167 | 168 | stream: stream, 169 | } 170 | 171 | // Call the OnSession hooks. 172 | err = h.hookOnSession(s, stream) 173 | if err != nil { 174 | return 175 | } 176 | 177 | // Reset the deadlines. Might be set by some hooks. 178 | err = stream.SetDeadline(time.Time{}) 179 | if err != nil { 180 | return 181 | } 182 | 183 | // Start the session routines. 184 | s.startRPCRoutines() 185 | 186 | // Call the OnSessionClosed hooks as soon as the session closes. 187 | s.OnClose(func() error { 188 | h.hookOnSessionClosed(s) 189 | return nil 190 | }) 191 | 192 | return 193 | } 194 | -------------------------------------------------------------------------------- /pkg/client/session_rpc read.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package client 29 | 30 | import ( 31 | "errors" 32 | "fmt" 33 | 34 | "github.com/desertbit/orbit/internal/api" 35 | "github.com/desertbit/orbit/internal/rpc" 36 | ) 37 | 38 | const ( 39 | numRPCReadRoutines = 3 40 | ) 41 | 42 | func (s *session) startRPCRoutines() { 43 | for i := 0; i < numRPCReadRoutines; i++ { 44 | go s.rpcReadRoutine() 45 | } 46 | } 47 | 48 | func (s *session) rpcReadRoutine() { 49 | // Close the session on exit. 50 | defer s.Close_() 51 | 52 | var ( 53 | err error 54 | reqType api.RPCType 55 | header []byte 56 | payload []byte 57 | ) 58 | 59 | for { 60 | s.streamReadMx.Lock() 61 | reqType, header, payload, err = rpc.Read(s.stream, nil, nil, s.maxHeaderSize, s.maxRetSize) 62 | s.streamReadMx.Unlock() 63 | if err != nil { 64 | // Log errors, but only, if the session or stream are not closing. 65 | if !s.IsClosing() && !s.stream.IsClosed() && !s.conn.IsClosedError(err) { 66 | s.log.Error(). 67 | Err(err). 68 | Msg("rpc: read routine") 69 | } 70 | return 71 | } 72 | 73 | s.handleRPCRequest(reqType, header, payload) 74 | } 75 | } 76 | 77 | func (s *session) handleRPCRequest(reqType api.RPCType, headerData, payloadData []byte) { 78 | var err error 79 | 80 | // Check the request type. 81 | // The client supports only a limited range of requests. 82 | switch reqType { 83 | case api.RPCTypeReturn: 84 | err = s.handleRPCReturn(headerData, payloadData) 85 | default: 86 | err = fmt.Errorf("invalid request type '%v'", reqType) 87 | } 88 | if err != nil { 89 | s.log.Error(). 90 | Err(err). 91 | Msg("rpc: failed to handle request") 92 | } 93 | } 94 | 95 | // handleReturn processes an incoming response with request type 'typeCallReturn'. 96 | // It decodes the header and uses it to retrieve the correct channel for this callReturn. 97 | // The response payload is then wrapped in a context and sent over the channel 98 | // back to the calling function that waits for it. 99 | // It can then deliver the response to the caller, which is the end of 100 | // one request-response cycle. 101 | func (s *session) handleRPCReturn(headerData, payloadData []byte) error { 102 | // Decode the header. 103 | var header api.RPCReturn 104 | err := api.Codec.Decode(headerData, &header) 105 | if err != nil { 106 | return fmt.Errorf("return request: decode header: %w", err) 107 | } 108 | 109 | // Get the channel by the key. 110 | channel := s.chain.Get(header.Key) 111 | if channel == nil { 112 | return fmt.Errorf("return request: no handler func available for key '%d'", header.Key) 113 | } 114 | 115 | // Create the channel data. 116 | rData := chainData{Data: payloadData} 117 | 118 | // Create an ErrorCode, if an error is present. 119 | if header.Err != "" { 120 | rData.Err = errImpl{msg: header.Err, code: header.ErrCode} 121 | } 122 | 123 | // Send the return data to the channel. 124 | select { 125 | case channel <- rData: 126 | return nil 127 | default: 128 | return errors.New("return request: failed to deliver return data: channel full") 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /pkg/client/session_rpc_cancel.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package client 29 | 30 | import ( 31 | "context" 32 | "fmt" 33 | "time" 34 | 35 | "github.com/desertbit/orbit/internal/api" 36 | "github.com/desertbit/orbit/pkg/transport" 37 | ) 38 | 39 | const ( 40 | openCancelStreamTimeout = 10 * time.Second 41 | cancelRequestTimeout = 3 * time.Second 42 | ) 43 | 44 | func (s *session) cancelCall(key uint32) error { 45 | // Open the cancel stream if not present. 46 | stream, err := s.openCancelStream() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | ctx, cancel := context.WithTimeout(context.Background(), cancelRequestTimeout) 52 | defer cancel() 53 | 54 | // Cancel the call on the remote peer. 55 | return s.writeRPCRequest(ctx, stream, &s.cancelStreamMx, api.RPCTypeCancel, &api.RPCCall{Key: key}, nil, 0) 56 | } 57 | 58 | func (s *session) openCancelStream() (stream transport.Stream, err error) { 59 | s.cancelStreamMx.Lock() 60 | defer s.cancelStreamMx.Unlock() 61 | 62 | // Check if the stream is already present. 63 | if s.cancelStream != nil && !s.cancelStream.IsClosed() { 64 | stream = s.cancelStream 65 | return 66 | } 67 | 68 | ctx, cancel := context.WithTimeout(context.Background(), openCancelStreamTimeout) 69 | defer cancel() 70 | 71 | stream, err = s.openStream(ctx, api.StreamTypeCancelCalls, nil, 0) 72 | if err != nil { 73 | err = fmt.Errorf("failed to open cancel stream: %w", err) 74 | return 75 | } 76 | s.cancelStream = stream 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /pkg/client/session_stream.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package client 29 | 30 | import ( 31 | "context" 32 | "time" 33 | 34 | "github.com/desertbit/orbit/internal/api" 35 | "github.com/desertbit/orbit/pkg/packet" 36 | "github.com/desertbit/orbit/pkg/transport" 37 | ) 38 | 39 | func (s *session) OpenTypedStream(ctx context.Context, id string, maxArgSize, maxRetSize int, wOnly bool) (ts TypedRWStream, err error) { 40 | // Use default options if required. 41 | if maxArgSize == DefaultMaxSize { 42 | maxArgSize = s.maxArgSize 43 | } 44 | if maxRetSize == DefaultMaxSize { 45 | maxRetSize = s.maxRetSize 46 | } 47 | 48 | // Open the raw stream. 49 | stream, err := s.OpenRawStream(ctx, id) 50 | if err != nil { 51 | return 52 | } 53 | 54 | // Create our typed stream. 55 | ts = newTypedRWStream(stream, s.codec, maxRetSize, maxArgSize, wOnly) 56 | return 57 | } 58 | 59 | func (s *session) OpenRawStream(ctx context.Context, id string) (stream transport.Stream, err error) { 60 | // Create a new client context. 61 | cctx := newContext(ctx, s) 62 | 63 | // Call the OnStream hooks. 64 | err = s.handler.hookOnStream(cctx, id) 65 | if err != nil { 66 | return 67 | } 68 | 69 | // Call the closed hook if an error occurs. 70 | defer func() { 71 | if err != nil { 72 | s.handler.hookOnStreamClosed(cctx, id) 73 | } 74 | }() 75 | 76 | // Open the stream. 77 | stream, err = s.openStream(ctx, api.StreamTypeRaw, &api.StreamRaw{ 78 | ID: id, 79 | Data: cctx.header, 80 | }, s.maxHeaderSize) 81 | if err != nil { 82 | return 83 | } 84 | 85 | // Call the closed hook once closed. 86 | go func() { 87 | select { 88 | case <-stream.ClosedChan(): 89 | case <-s.ClosingChan(): 90 | } 91 | s.handler.hookOnStreamClosed(cctx, id) 92 | }() 93 | return 94 | } 95 | 96 | func (s *session) openStream( 97 | ctx context.Context, 98 | streamType api.StreamType, 99 | header interface{}, 100 | maxHeaderSize int, 101 | ) ( 102 | stream transport.Stream, 103 | err error, 104 | ) { 105 | // Open the stream through our conn. 106 | stream, err = s.conn.OpenStream(ctx) 107 | if err != nil { 108 | return 109 | } 110 | defer func() { 111 | if err != nil { 112 | _ = stream.Close() 113 | } 114 | }() 115 | 116 | // Set a write deadline, if needed. 117 | if deadline, ok := ctx.Deadline(); ok { 118 | err = stream.SetWriteDeadline(deadline) 119 | if err != nil { 120 | return 121 | } 122 | } 123 | 124 | // Write the stream type. 125 | _, err = stream.Write([]byte{byte(streamType)}) 126 | if err != nil { 127 | return 128 | } 129 | 130 | // Write the header if set. 131 | if header != nil { 132 | err = packet.WriteEncode(stream, header, api.Codec, maxHeaderSize) 133 | if err != nil { 134 | return 135 | } 136 | } 137 | 138 | // Reset the deadlines. 139 | err = stream.SetDeadline(time.Time{}) 140 | return 141 | } 142 | -------------------------------------------------------------------------------- /pkg/codec/codec.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | /* 29 | Package codec contains sub-packages with different codecs that can be used 30 | to encode/decode any entity to/from a byte stream. 31 | */ 32 | package codec 33 | 34 | // Codec represents a codec used to encode and decode entities. 35 | type Codec interface { 36 | // Encode encodes the value to a byte slice. 37 | Encode(v interface{}) ([]byte, error) 38 | 39 | // Decode decodes the byte slice into the value. 40 | Decode(b []byte, v interface{}) error 41 | } 42 | -------------------------------------------------------------------------------- /pkg/codec/codec_tester.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package codec 29 | 30 | import ( 31 | "encoding/gob" 32 | "testing" 33 | "time" 34 | 35 | "github.com/stretchr/testify/require" 36 | ) 37 | 38 | type test struct { 39 | Name string 40 | } 41 | 42 | // Tester is a test helper to test a Codec. 43 | // It encodes a test struct using the given codec and decodes 44 | // it into a second test struct afterwards. 45 | // It then uses the reflect pkg to check if both structs have 46 | // the exact same values. 47 | func Tester(t *testing.T, c Codec) { 48 | // Struct. 49 | ssrc := &test{Name: "test"} 50 | var sdst *test 51 | encoded, err := c.Encode(ssrc) 52 | require.NoError(t, err) 53 | 54 | err = c.Decode(encoded, &sdst) 55 | require.NoError(t, err) 56 | require.Exactly(t, ssrc, sdst) 57 | 58 | // Int. 59 | isrc := 5 60 | var idst int 61 | encoded, err = c.Encode(isrc) 62 | require.NoError(t, err) 63 | 64 | err = c.Decode(encoded, &idst) 65 | require.NoError(t, err) 66 | require.Exactly(t, isrc, idst) 67 | 68 | // Map. 69 | msrc := map[string]float32{"test": 0.85} 70 | var mdst map[string]float32 71 | encoded, err = c.Encode(msrc) 72 | require.NoError(t, err) 73 | 74 | err = c.Decode(encoded, &mdst) 75 | require.NoError(t, err) 76 | require.Exactly(t, msrc, mdst) 77 | 78 | // Slice. 79 | slsrc := []rune{85, 48, 68} 80 | var sldst []rune 81 | encoded, err = c.Encode(slsrc) 82 | require.NoError(t, err) 83 | 84 | err = c.Decode(encoded, &sldst) 85 | require.NoError(t, err) 86 | require.Exactly(t, slsrc, sldst) 87 | 88 | // time.Time. 89 | tsrc := time.Unix(584846, 471448) 90 | var tdst time.Time 91 | encoded, err = c.Encode(tsrc) 92 | require.NoError(t, err) 93 | 94 | err = c.Decode(encoded, &tdst) 95 | require.NoError(t, err) 96 | require.True(t, tsrc.Equal(tdst)) 97 | } 98 | 99 | func init() { 100 | gob.Register(&test{}) 101 | } 102 | -------------------------------------------------------------------------------- /pkg/codec/json/json.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | /* 29 | Package json offers an implementation of the codec.Codec interface 30 | for the json data format. It uses the https://golang.org/pkg/encoding/json/ 31 | pkg to en-/decode an entity to/from a byte slice. 32 | */ 33 | package json 34 | 35 | import "encoding/json" 36 | 37 | // Codec that encodes to and decodes from JSON. 38 | var Codec = &jsonCodec{} 39 | 40 | // The jsonCodec type is a private dummy struct used 41 | // to implement the codec.Codec interface using JSON. 42 | type jsonCodec struct{} 43 | 44 | // Implements the codec.Codec interface. 45 | // It uses the json.Marshal func. 46 | func (j *jsonCodec) Encode(v interface{}) ([]byte, error) { 47 | return json.Marshal(v) 48 | } 49 | 50 | // Implements the codec.Codec interface. 51 | // It uses the json.Unmarshal func. 52 | func (j *jsonCodec) Decode(b []byte, v interface{}) error { 53 | return json.Unmarshal(b, v) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/codec/json/json_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package json_test 29 | 30 | import ( 31 | "testing" 32 | 33 | "github.com/desertbit/orbit/pkg/codec" 34 | "github.com/desertbit/orbit/pkg/codec/json" 35 | ) 36 | 37 | func TestJSON(t *testing.T) { 38 | codec.Tester(t, json.Codec) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/codec/msgpack/msgpack.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | /* 29 | Package msgpack offers an implementation of the codec.Codec interface 30 | for the msgpack data format. 31 | 32 | It uses the faster https://github.com/tinylib/msgp/msgp Un-/Marshaler, 33 | if it is implemented on the entity. Otherwise, it falls back to 34 | using the Un-/Marshal funcs from the https://gopkg.in/vmihailenco/msgpack.v3 package. 35 | */ 36 | package msgpack 37 | 38 | import ( 39 | "github.com/tinylib/msgp/msgp" 40 | msgpack "gopkg.in/vmihailenco/msgpack.v3" 41 | ) 42 | 43 | // Codec that encodes to and decodes from msgpack. 44 | var Codec = &msgpackCodec{} 45 | 46 | // The msgpackCodec type is a private dummy struct used 47 | // to implement the codec.Codec interface using msgpack. 48 | type msgpackCodec struct{} 49 | 50 | // Implements the codec.Codec interface. 51 | // It uses the faster msgp.Marshaler if implemented. 52 | func (mc *msgpackCodec) Encode(v interface{}) ([]byte, error) { 53 | if d, ok := v.(msgp.Marshaler); ok { 54 | return d.MarshalMsg(nil) 55 | } 56 | 57 | return msgpack.Marshal(v) 58 | } 59 | 60 | // Implements the codec.Codec interface. 61 | // It uses the faster msgp.Unmarshaler if implemented. 62 | func (mc *msgpackCodec) Decode(b []byte, v interface{}) error { 63 | if d, ok := v.(msgp.Unmarshaler); ok { 64 | _, err := d.UnmarshalMsg(b) 65 | return err 66 | } 67 | 68 | return msgpack.Unmarshal(b, v) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/hook/log/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package log 29 | 30 | import ( 31 | "errors" 32 | "os" 33 | "time" 34 | 35 | "github.com/desertbit/orbit/pkg/client" 36 | "github.com/desertbit/orbit/pkg/transport" 37 | "github.com/rs/zerolog" 38 | ) 39 | 40 | type clientHook struct { 41 | log *zerolog.Logger 42 | } 43 | 44 | func ClientHook(logger ...*zerolog.Logger) client.Hook { 45 | c := &clientHook{} 46 | 47 | if len(logger) > 0 { 48 | c.log = logger[0] 49 | } else { 50 | l := zerolog.New(zerolog.ConsoleWriter{ 51 | Out: os.Stderr, 52 | TimeFormat: time.RFC3339, 53 | }).With().Timestamp().Str("component", "orbit").Logger() 54 | c.log = &l 55 | } 56 | return c 57 | } 58 | 59 | func (c *clientHook) Close() error { 60 | c.log.Info().Msg("client closed") 61 | return nil 62 | } 63 | 64 | func (c *clientHook) OnSession(s client.Session, stream transport.Stream) error { 65 | c.log.Info(). 66 | Str("sessionID", s.ID()). 67 | Str("localAddr", s.LocalAddr().String()). 68 | Str("remoteAddr", s.RemoteAddr().String()). 69 | Msg("new session") 70 | return nil 71 | } 72 | 73 | func (c *clientHook) OnSessionClosed(s client.Session) { 74 | c.log.Info(). 75 | Str("sessionID", s.ID()). 76 | Str("localAddr", s.LocalAddr().String()). 77 | Str("remoteAddr", s.RemoteAddr().String()). 78 | Msg("session closed") 79 | } 80 | 81 | func (c *clientHook) OnCall(ctx client.Context, id string, callKey uint32) error { 82 | s := ctx.Session() 83 | c.log.Debug(). 84 | Str("callID", id). 85 | Uint32("callKey", callKey). 86 | Str("sessionID", s.ID()). 87 | Str("localAddr", s.LocalAddr().String()). 88 | Str("remoteAddr", s.RemoteAddr().String()). 89 | Msg("call") 90 | return nil 91 | } 92 | 93 | func (c *clientHook) OnCallDone(ctx client.Context, id string, callKey uint32, err error) { 94 | s := ctx.Session() 95 | 96 | if err == nil { 97 | c.log.Debug(). 98 | Str("callID", id). 99 | Uint32("callKey", callKey). 100 | Str("sessionID", s.ID()). 101 | Str("localAddr", s.LocalAddr().String()). 102 | Str("remoteAddr", s.RemoteAddr().String()). 103 | Msg("call done") 104 | return 105 | } 106 | 107 | // Check, if an orbit client error was returned. 108 | var oErr client.Error 109 | if errors.As(err, &oErr) { 110 | c.log.Error(). 111 | Err(err). 112 | Int("errCode", oErr.Code()). 113 | Str("callID", id). 114 | Uint32("callKey", callKey). 115 | Str("sessionID", s.ID()). 116 | Str("localAddr", s.LocalAddr().String()). 117 | Str("remoteAddr", s.RemoteAddr().String()). 118 | Msg("call failed") 119 | } else { 120 | c.log.Error(). 121 | Err(err). 122 | Str("callID", id). 123 | Uint32("callKey", callKey). 124 | Str("sessionID", s.ID()). 125 | Str("localAddr", s.LocalAddr().String()). 126 | Str("remoteAddr", s.RemoteAddr().String()). 127 | Msg("call failed") 128 | } 129 | } 130 | 131 | func (c *clientHook) OnCallCanceled(ctx client.Context, id string, callKey uint32) { 132 | s := ctx.Session() 133 | c.log.Debug(). 134 | Str("callID", id). 135 | Uint32("callKey", callKey). 136 | Str("sessionID", s.ID()). 137 | Str("localAddr", s.LocalAddr().String()). 138 | Str("remoteAddr", s.RemoteAddr().String()). 139 | Msg("call canceled") 140 | } 141 | 142 | func (c *clientHook) OnStream(ctx client.Context, id string) error { 143 | s := ctx.Session() 144 | c.log.Debug(). 145 | Str("streamID", id). 146 | Str("sessionID", s.ID()). 147 | Str("localAddr", s.LocalAddr().String()). 148 | Str("remoteAddr", s.RemoteAddr().String()). 149 | Msg("stream") 150 | return nil 151 | } 152 | 153 | func (c *clientHook) OnStreamClosed(ctx client.Context, id string) { 154 | s := ctx.Session() 155 | c.log.Debug(). 156 | Str("streamID", id). 157 | Str("sessionID", s.ID()). 158 | Str("localAddr", s.LocalAddr().String()). 159 | Str("remoteAddr", s.RemoteAddr().String()). 160 | Msg("stream closed") 161 | } 162 | -------------------------------------------------------------------------------- /pkg/service/context.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package service 29 | 30 | import "context" 31 | 32 | // A Context defines the service context which extends the context.Context interface. 33 | type Context interface { 34 | context.Context 35 | 36 | // SetContext can be used to wrap the context.Context with additonal deadlines, ... 37 | SetContext(ctx context.Context) 38 | 39 | // Session returns the current active session. 40 | Session() Session 41 | 42 | // Header returns the raw header byte slice defined by the key. Returns nil if not present. 43 | Header(key string) []byte 44 | 45 | // Data returns the value defined by the key. Returns nil if not present. 46 | Data(key string) interface{} 47 | 48 | // SetData sets the value defined by the key. 49 | SetData(key string, v interface{}) 50 | } 51 | 52 | type serviceContext struct { 53 | context.Context 54 | 55 | s Session 56 | header map[string][]byte 57 | data map[string]interface{} 58 | } 59 | 60 | func newContext(ctx context.Context, s Session, header map[string][]byte) Context { 61 | return &serviceContext{ 62 | Context: ctx, 63 | s: s, 64 | header: header, 65 | } 66 | } 67 | 68 | func (c *serviceContext) SetContext(ctx context.Context) { 69 | c.Context = ctx 70 | } 71 | 72 | func (c *serviceContext) Session() Session { 73 | return c.s 74 | } 75 | 76 | func (c *serviceContext) Header(key string) []byte { 77 | return c.header[key] 78 | } 79 | 80 | func (c *serviceContext) Data(key string) interface{} { 81 | if c.data == nil { 82 | return nil 83 | } 84 | return c.data[key] 85 | } 86 | 87 | func (c *serviceContext) SetData(key string, v interface{}) { 88 | if c.data == nil { 89 | c.data = make(map[string]interface{}) 90 | } 91 | c.data[key] = v 92 | } 93 | -------------------------------------------------------------------------------- /pkg/service/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package service 29 | 30 | import ( 31 | "errors" 32 | ) 33 | 34 | var ( 35 | // ErrClosed defines the error if a stream, session or service is closed. 36 | ErrClosed = errors.New("closed") 37 | 38 | // ErrInvalidVersion defines the error if the version of both peers do not match 39 | // during the version exchange. 40 | ErrInvalidVersion = errors.New("invalid version") 41 | 42 | // ErrCatchedPanic defines the error if a panic has been catched while executing user code. 43 | ErrCatchedPanic = errors.New("catched panic") 44 | ) 45 | 46 | // An Error offers a way for handler functions of rpc calls to 47 | // determine the information passed to the client, in case an error 48 | // occurs. That way, sensitive information that may be contained in 49 | // a standard error, can be hidden from the client. 50 | // Instead, a Msg and a code can be sent back to give a non-sensitive 51 | // explanation of the error and a code that is easy to check, to 52 | // allow handling common errors. 53 | type Error interface { 54 | // Embeds the standard go error interface. 55 | error 56 | 57 | // Msg returns a textual explanation of the error and should 58 | // NOT contain sensitive information about the application. 59 | Msg() string 60 | 61 | // Code returns an integer that can give a hint about the 62 | // type of error that occurred. 63 | Code() int 64 | } 65 | 66 | // NewError constructs and returns a type that satisfies the Error interface 67 | // from the given parameters. 68 | func NewError(err error, msg string, code int) Error { 69 | return errImpl{ 70 | err: err, 71 | msg: msg, 72 | code: code, 73 | } 74 | } 75 | 76 | // Implements the Error interface. 77 | type errImpl struct { 78 | err error 79 | msg string 80 | code int 81 | } 82 | 83 | func (e errImpl) Error() string { 84 | if e.err == nil { 85 | return "" 86 | } 87 | return e.err.Error() 88 | } 89 | 90 | func (e errImpl) Msg() string { 91 | return e.msg 92 | } 93 | 94 | func (e errImpl) Code() int { 95 | return e.code 96 | } 97 | -------------------------------------------------------------------------------- /pkg/service/hook.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package service 29 | 30 | import ( 31 | "github.com/desertbit/orbit/pkg/transport" 32 | ) 33 | 34 | type Hooks []Hook 35 | 36 | // Hook allows third-party code to hook into orbit's logic, to implement for example 37 | // logging or authentication functionality. 38 | type Hook interface { 39 | // Close is called if the service closes. 40 | Close() error 41 | 42 | // OnSession is called if a new client session is connected to the service. 43 | // RPC and stream routines are handled after this hook. 44 | // Do not use the stream after returning from this hook. 45 | // Return an error to close the session and abort the initialization process. 46 | OnSession(s Session, stream transport.Stream) error 47 | 48 | // OnSessionClosed is called as soon as the session closes. 49 | OnSessionClosed(s Session) 50 | 51 | // OnCall is called before a call request. 52 | // Return an error to abort the call. 53 | OnCall(ctx Context, id string, callKey uint32) error 54 | 55 | // OnCallDone is called after a call request. 56 | // The context is the same as from the OnCall hook. 57 | // If err == nil, then the call completed successfully. 58 | OnCallDone(ctx Context, id string, callKey uint32, err error) 59 | 60 | // OnCallCanceled is called, if a call is canceled. 61 | // The context is the same as from the OnCall hook. 62 | OnCallCanceled(ctx Context, id string, callKey uint32) 63 | 64 | // OnStream is called during a new stream setup. 65 | // Return an error to abort the stream setup. 66 | OnStream(ctx Context, id string) error 67 | 68 | // OnStreamClosed is called after a stream closes. 69 | // The context is the same as from the OnStream hook. 70 | // If err == nil, then the stream completed successfully. 71 | OnStreamClosed(ctx Context, id string, err error) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/service/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package service 29 | 30 | import ( 31 | "errors" 32 | "os" 33 | "time" 34 | 35 | "github.com/desertbit/closer/v3" 36 | "github.com/desertbit/orbit/pkg/codec" 37 | "github.com/desertbit/orbit/pkg/codec/msgpack" 38 | "github.com/desertbit/orbit/pkg/transport" 39 | "github.com/rs/zerolog" 40 | ) 41 | 42 | const ( 43 | defaultCallTimeout = 30 * time.Second 44 | defaultHandshakeTimeout = 7 * time.Second 45 | defaultAcceptConnWorkers = 5 46 | defaultSessionIDLen = 32 47 | 48 | defaultMaxArgSize = 4 * 1024 * 1024 // 4 MB 49 | defaultMaxRetSize = 4 * 1024 * 1024 // 4 MB 50 | defaultMaxHeaderSize = 500 * 1024 // 500 KB 51 | ) 52 | 53 | type Options struct { 54 | // ListenAddr specifies the listen address for the server. This value is passed to the transport backend. 55 | ListenAddr string 56 | 57 | // Transport specifies the communication backend. This value must be set. 58 | Transport transport.Transport 59 | 60 | // Optional values: 61 | // ################ 62 | 63 | // Closer defines the closer instance. A default closer will be created if unspecified. 64 | Closer closer.Closer 65 | 66 | // Codec defines the transport encoding. A default codec will be used if unspecified. 67 | Codec codec.Codec 68 | 69 | // Hooks specifies the hooks executed during certain actions. The order of the hooks is stricly followed. 70 | Hooks Hooks 71 | 72 | // Log specifies the default logger backend. A default logger will be used if unspecified. 73 | Log *zerolog.Logger 74 | 75 | // CallTimeout specifies the default timeout for each call. 76 | // Set to -1 (NoTimeout) for no timeout. 77 | CallTimeout time.Duration 78 | 79 | // HandshakeTimeout specifies the timeout for the initial handshake. 80 | HandshakeTimeout time.Duration 81 | 82 | // AcceptConnWorkers specifies the routines accepting new connections. 83 | AcceptConnWorkers int 84 | 85 | // SessionIDLen specifies the length of each session ID. 86 | SessionIDLen uint 87 | 88 | // PrintPanicStackTraces prints stack traces of catched panics. 89 | PrintPanicStackTraces bool 90 | 91 | // SendInternalErrors sends all errors to the client, even if the Error interface is not satisfied. 92 | SendInternalErrors bool 93 | 94 | // MaxArgSize defines the default maximum argument payload size for RPC calls. 95 | MaxArgSize int 96 | 97 | // MaxRetSize defines the default maximum return payload size for RPC calls. 98 | MaxRetSize int 99 | 100 | // MaxHeaderSize defines the maximum header size for calls and streams. 101 | MaxHeaderSize int 102 | } 103 | 104 | func (o *Options) setDefaults() { 105 | if o.Closer == nil { 106 | o.Closer = closer.New() 107 | } 108 | if o.Codec == nil { 109 | o.Codec = msgpack.Codec 110 | } 111 | if o.Log == nil { 112 | l := zerolog.New(zerolog.ConsoleWriter{ 113 | Out: os.Stderr, 114 | TimeFormat: time.RFC3339, 115 | }).With().Timestamp().Str("component", "orbit").Logger() 116 | o.Log = &l 117 | } 118 | if o.CallTimeout == 0 { 119 | o.CallTimeout = defaultCallTimeout 120 | } 121 | if o.HandshakeTimeout == 0 { 122 | o.HandshakeTimeout = defaultHandshakeTimeout 123 | } 124 | if o.AcceptConnWorkers == 0 { 125 | o.AcceptConnWorkers = defaultAcceptConnWorkers 126 | } 127 | if o.SessionIDLen == 0 { 128 | o.SessionIDLen = defaultSessionIDLen 129 | } 130 | if o.MaxArgSize == 0 { 131 | o.MaxArgSize = defaultMaxArgSize 132 | } 133 | if o.MaxRetSize == 0 { 134 | o.MaxRetSize = defaultMaxRetSize 135 | } 136 | if o.MaxHeaderSize == 0 { 137 | o.MaxHeaderSize = defaultMaxHeaderSize 138 | } 139 | } 140 | 141 | func (o *Options) validate() error { 142 | if o.ListenAddr == "" { 143 | return errors.New("empty listen address") 144 | } else if o.Transport == nil { 145 | return errors.New("no transport set") 146 | } 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /pkg/service/service_conn.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package service 29 | 30 | import ( 31 | "fmt" 32 | 33 | "github.com/desertbit/orbit/internal/strutil" 34 | "github.com/desertbit/orbit/pkg/transport" 35 | ) 36 | 37 | func (s *service) startAcceptConnRoutines() { 38 | for i := 0; i < s.opts.AcceptConnWorkers; i++ { 39 | go s.handleNewConnRoutine() 40 | } 41 | } 42 | 43 | func (s *service) handleNewConnRoutine() { 44 | var ( 45 | err error 46 | closingChan = s.ClosingChan() 47 | ) 48 | 49 | for { 50 | select { 51 | case <-closingChan: 52 | return 53 | 54 | case conn := <-s.newConnChan: 55 | err = s.handleNewConn(conn) 56 | if err != nil { 57 | s.log.Error(). 58 | Err(err). 59 | Msg("service: handle new conn") 60 | } 61 | } 62 | } 63 | } 64 | 65 | func (s *service) handleNewConn(conn transport.Conn) (err error) { 66 | // Generate an id for the session. 67 | id, err := strutil.RandomString(s.opts.SessionIDLen) 68 | if err != nil { 69 | return 70 | } 71 | 72 | // Create a new server session. 73 | sn, err := initSession(conn, id, s, s.opts) 74 | if err != nil { 75 | return 76 | } 77 | 78 | // Save the session in the map. 79 | // If the id already exists, close the new session instead. 80 | // This will happen almost never, if session id len is large enough. 81 | var idExists bool 82 | s.sessionsMx.Lock() 83 | _, idExists = s.sessions[sn.id] 84 | if !idExists { 85 | s.sessions[sn.id] = sn 86 | } 87 | s.sessionsMx.Unlock() 88 | 89 | // Close the new session, if its id is already taken. 90 | if idExists { 91 | sn.Close_() 92 | return fmt.Errorf("closed new session with duplicate session ID: %s", id) 93 | } 94 | 95 | // Remove the session from the session map, once it closes. 96 | sn.OnClosing(func() error { 97 | // Speed up the closing process if the server closes. 98 | if s.IsClosing() { 99 | return nil 100 | } 101 | 102 | s.sessionsMx.Lock() 103 | delete(s.sessions, sn.id) 104 | s.sessionsMx.Unlock() 105 | return nil 106 | }) 107 | return 108 | } 109 | -------------------------------------------------------------------------------- /pkg/service/session_rpc_async.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package service 29 | 30 | import ( 31 | "fmt" 32 | 33 | "github.com/desertbit/orbit/internal/api" 34 | "github.com/desertbit/orbit/internal/rpc" 35 | "github.com/desertbit/orbit/pkg/transport" 36 | ) 37 | 38 | func (s *session) handleAsyncCallStream(stream transport.Stream, id string) error { 39 | // Always close the stream. 40 | defer stream.Close() 41 | 42 | // Get the options for this call. 43 | opts, err := s.handler.getAsyncCallOptions(id) 44 | if err != nil { 45 | return fmt.Errorf("async call: %w", err) 46 | } 47 | 48 | // Read the single async request from the stream. 49 | reqType, header, payload, err := rpc.Read(stream, nil, nil, s.maxHeaderSize, opts.maxArgSize) 50 | if err != nil { 51 | return fmt.Errorf("async call: read failed: %w", err) 52 | } else if reqType != api.RPCTypeCall { 53 | return fmt.Errorf("async call: invalid request type: %v", reqType) 54 | } 55 | 56 | // Handle it like a normal call. 57 | err = s.handleCall(stream, nil, header, payload, opts.maxRetSize) 58 | if err != nil { 59 | return fmt.Errorf("async: %w", err) 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/service/session_rpc_cancel.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package service 29 | 30 | import ( 31 | "context" 32 | "errors" 33 | "fmt" 34 | "time" 35 | 36 | "github.com/desertbit/orbit/internal/api" 37 | 38 | "github.com/desertbit/orbit/internal/rpc" 39 | "github.com/desertbit/orbit/pkg/transport" 40 | ) 41 | 42 | const ( 43 | earlyCancelLifetime = 10 * time.Second 44 | maxCancelHeaderSize = 1024 // 1 KB 45 | ) 46 | 47 | func (s *session) handleCancelStream(stream transport.Stream) (err error) { 48 | // Close stream on error. 49 | defer func() { 50 | if err != nil { 51 | stream.Close() 52 | } 53 | }() 54 | 55 | // Abort if a cancel stream is already present. 56 | var hasStream bool 57 | s.cancelMx.Lock() 58 | hasStream = s.hasCancelStream 59 | s.hasCancelStream = true 60 | s.cancelMx.Unlock() 61 | if hasStream { 62 | return errors.New("a cancel stream is already present") 63 | } 64 | 65 | // Start the read routine. 66 | go s.rpcCancelReadRoutine(stream) 67 | return nil 68 | } 69 | 70 | func (s *session) rpcCancelReadRoutine(stream transport.Stream) { 71 | // Close the session on exit. 72 | // Currently only one cancel stream is supported. 73 | defer s.Close_() 74 | 75 | var ( 76 | err error 77 | reqType api.RPCType 78 | header []byte 79 | ) 80 | 81 | for { 82 | // Read and reuse the header buffer. 83 | reqType, header, _, err = rpc.Read(stream, header, nil, maxCancelHeaderSize, 0) 84 | if err != nil { 85 | // Log errors, but only, if the session or stream are not closing. 86 | if !s.IsClosing() && !stream.IsClosed() && !s.conn.IsClosedError(err) { 87 | s.log.Error(). 88 | Err(err). 89 | Msg("rpc: cancel read routine") 90 | } 91 | return 92 | } 93 | 94 | err = s.handleCancel(reqType, header) 95 | if err != nil { 96 | s.log.Warn(). 97 | Err(err). 98 | Msg("rpc: failed to handle cancel request") 99 | continue 100 | } 101 | } 102 | } 103 | 104 | func (s *session) handleCancel(reqType api.RPCType, header []byte) (err error) { 105 | // Ensure the type is valid. 106 | if reqType != api.RPCTypeCancel { 107 | return fmt.Errorf("invalid request type: %v: expected cancel type", reqType) 108 | } 109 | 110 | // Decode the request header. 111 | var h api.RPCCancel 112 | err = api.Codec.Decode(header, &h) 113 | if err != nil { 114 | return fmt.Errorf("decode header: %w", err) 115 | } 116 | 117 | var ( 118 | ok bool 119 | cancel context.CancelFunc 120 | closeChan = make(chan struct{}) 121 | ) 122 | 123 | // Obtain the cancel function if present. 124 | // If not present, then this cancel request might have arrived before the actual call request. 125 | // Notify the call to do an early cancel. 126 | s.cancelMx.Lock() 127 | cancel, ok = s.cancelCalls[h.Key] 128 | if !ok { 129 | s.cancelCalls[h.Key] = func() { 130 | close(closeChan) 131 | } 132 | } 133 | s.cancelMx.Unlock() 134 | 135 | // Cancel the call if possible. 136 | if ok { 137 | cancel() 138 | } else { 139 | // Tidy up the early cancel value from the map after a timeout. 140 | go func() { 141 | t := time.NewTimer(earlyCancelLifetime) 142 | defer t.Stop() 143 | 144 | select { 145 | case <-closeChan: 146 | return 147 | case <-t.C: 148 | s.deleteCancelFunc(h.Key) 149 | } 150 | }() 151 | } 152 | 153 | return 154 | } 155 | 156 | func (s *session) setCancelFunc(key uint32, f context.CancelFunc) (alreadyCanceled bool) { 157 | var ff context.CancelFunc 158 | 159 | s.cancelMx.Lock() 160 | ff, alreadyCanceled = s.cancelCalls[key] 161 | if alreadyCanceled { 162 | delete(s.cancelCalls, key) 163 | } else { 164 | s.cancelCalls[key] = f 165 | } 166 | s.cancelMx.Unlock() 167 | 168 | // Call the function which was present in the map. 169 | if alreadyCanceled { 170 | ff() 171 | } 172 | return 173 | } 174 | 175 | func (s *session) deleteCancelFunc(key uint32) { 176 | s.cancelMx.Lock() 177 | delete(s.cancelCalls, key) 178 | s.cancelMx.Unlock() 179 | } 180 | -------------------------------------------------------------------------------- /pkg/transport/quic/listener.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package quic 29 | 30 | import ( 31 | "context" 32 | "net" 33 | 34 | "github.com/desertbit/closer/v3" 35 | "github.com/desertbit/orbit/pkg/transport" 36 | quic "github.com/quic-go/quic-go" 37 | ) 38 | 39 | var _ transport.Listener = &listener{} 40 | 41 | type listener struct { 42 | closer.Closer 43 | 44 | ln *quic.Listener 45 | } 46 | 47 | func newListener(cl closer.Closer, ln *quic.Listener) transport.Listener { 48 | l := &listener{ 49 | Closer: cl, 50 | ln: ln, 51 | } 52 | l.OnClosing(ln.Close) 53 | return l 54 | } 55 | 56 | // Implements the transport.Listener interface. 57 | func (l *listener) Accept() (transport.Conn, error) { 58 | // No context passed, as listener is closed in onClosing of closer. 59 | qs, err := l.ln.Accept(context.Background()) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | return newSession(l.CloserOneWay(), qs) 65 | } 66 | 67 | // Implements the transport.Listener interface. 68 | func (l *listener) Addr() net.Addr { 69 | return l.ln.Addr() 70 | } 71 | -------------------------------------------------------------------------------- /pkg/transport/quic/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package quic 29 | 30 | import ( 31 | "crypto/tls" 32 | "errors" 33 | "time" 34 | 35 | quic "github.com/quic-go/quic-go" 36 | ) 37 | 38 | var ( 39 | defaultConfig = &quic.Config{ 40 | HandshakeIdleTimeout: 5 * time.Second, 41 | MaxIdleTimeout: 30 * time.Second, 42 | MaxStreamReceiveWindow: 6 * 1024 * 1024, // 6MB 43 | MaxConnectionReceiveWindow: 15 * 1024 * 1024, // 15MB 44 | KeepAlivePeriod: 15 * time.Second, 45 | } 46 | ) 47 | 48 | type Options struct { 49 | // TODO: 50 | Config *quic.Config 51 | 52 | // TODO: 53 | TLSConfig *tls.Config 54 | } 55 | 56 | func (o *Options) setDefaults() { 57 | if o.Config == nil { 58 | o.Config = defaultConfig 59 | } 60 | } 61 | 62 | func (o *Options) validate() (err error) { 63 | if o.TLSConfig == nil { 64 | return errors.New("tls config must not be nil") 65 | } 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /pkg/transport/quic/session.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package quic 29 | 30 | import ( 31 | "context" 32 | "errors" 33 | "net" 34 | 35 | "github.com/desertbit/closer/v3" 36 | "github.com/desertbit/orbit/pkg/transport" 37 | quic "github.com/quic-go/quic-go" 38 | ) 39 | 40 | const ( 41 | errorCodeClose = 0x1 42 | ) 43 | 44 | var _ transport.Conn = &session{} 45 | 46 | type session struct { 47 | closer.Closer 48 | 49 | qs quic.Connection 50 | la net.Addr 51 | ra net.Addr 52 | } 53 | 54 | func newSession(cl closer.Closer, qs quic.Connection) (s *session, err error) { 55 | s = &session{ 56 | Closer: cl, 57 | qs: qs, 58 | la: qs.LocalAddr(), 59 | ra: qs.RemoteAddr(), 60 | } 61 | s.OnClosing(func() error { 62 | return qs.CloseWithError(errorCodeClose, "closed") 63 | }) 64 | 65 | // Always close on error. 66 | // Uncomment this, if code with errors is added! 67 | /*defer func() { 68 | if err != nil { 69 | s.Close_() 70 | } 71 | }()*/ 72 | 73 | // Always close if the quic session closes. 74 | go func() { 75 | select { 76 | case <-s.ClosingChan(): 77 | case <-qs.Context().Done(): 78 | } 79 | s.Close_() 80 | }() 81 | 82 | return 83 | } 84 | 85 | // Implements the transport.Conn interface. 86 | func (s *session) AcceptStream(ctx context.Context) (transport.Stream, error) { 87 | stream, err := s.qs.AcceptStream(ctx) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | return newStream(stream, s.la, s.ra), nil 93 | } 94 | 95 | // Implements the transport.Conn interface. 96 | func (s *session) OpenStream(ctx context.Context) (transport.Stream, error) { 97 | // We are using OpenStream instead of OpenStreamSync. 98 | // No context is required, because the stream is instantly opened. 99 | stream, err := s.qs.OpenStream() 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | return newStream(stream, s.la, s.ra), nil 105 | } 106 | 107 | // Implements the transport.Conn interface. 108 | func (s *session) LocalAddr() net.Addr { 109 | return s.la 110 | } 111 | 112 | // Implements the transport.Conn interface. 113 | func (s *session) RemoteAddr() net.Addr { 114 | return s.ra 115 | } 116 | 117 | func (s *session) IsClosedError(err error) bool { 118 | var sErr *quic.StreamError 119 | if errors.As(err, &sErr) { 120 | return sErr.ErrorCode == errorCodeClose 121 | } 122 | return false 123 | } 124 | -------------------------------------------------------------------------------- /pkg/transport/quic/stream.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package quic 29 | 30 | import ( 31 | "errors" 32 | "io" 33 | "net" 34 | 35 | "github.com/desertbit/orbit/pkg/transport" 36 | quic "github.com/quic-go/quic-go" 37 | ) 38 | 39 | var _ transport.Stream = &stream{} 40 | 41 | // Type stream wraps a quic.Stream and implements the 42 | // two missing methods so it can be used as a net.Conn. 43 | type stream struct { 44 | quic.Stream 45 | 46 | la net.Addr 47 | ra net.Addr 48 | } 49 | 50 | // newStream creates a new stream. 51 | func newStream(qs quic.Stream, la, ra net.Addr) *stream { 52 | return &stream{ 53 | Stream: qs, 54 | la: la, 55 | ra: ra, 56 | } 57 | } 58 | 59 | // Implements the transport.Stream interface. 60 | func (s *stream) LocalAddr() net.Addr { 61 | return s.la 62 | } 63 | 64 | // Implements the transport.Stream interface. 65 | func (s *stream) RemoteAddr() net.Addr { 66 | return s.ra 67 | } 68 | 69 | // Implements the transport.Stream interface. 70 | func (s *stream) Read(b []byte) (n int, err error) { 71 | // We want the quic.Stream to behave like a net.Conn. 72 | // Since the quic.Stream implements the io.Reader interface, it may return 73 | // an io.EOF error also, if at least one byte could be read. 74 | // But we only want io.EOF, if the connection closed, which is the case, if no bytes 75 | // were read. 76 | n, err = s.Stream.Read(b) 77 | if n > 0 && err == io.EOF { 78 | // Ignore io.EOF, if at least one byte could be read. 79 | err = nil 80 | } 81 | return 82 | } 83 | 84 | // Implements the transport.Stream interface. 85 | func (s *stream) Write(p []byte) (n int, err error) { 86 | // Check for the close error code from a CancelRead peer call. 87 | n, err = s.Stream.Write(p) 88 | if err != nil { 89 | var sErr *quic.StreamError 90 | if errors.As(err, &sErr) && sErr.ErrorCode == errorCodeClose { 91 | err = io.EOF 92 | } 93 | return 94 | } 95 | return 96 | } 97 | 98 | // Implements the transport.Stream interface. 99 | func (s *stream) Close() error { 100 | // Close the peer's writer, because Stream.Close does only a one way close. 101 | s.Stream.CancelRead(errorCodeClose) 102 | return s.Stream.Close() 103 | } 104 | 105 | // Implements the transport.Stream interface. 106 | func (s *stream) IsClosed() bool { 107 | select { 108 | case <-s.Context().Done(): 109 | return true 110 | default: 111 | return false 112 | } 113 | } 114 | 115 | // Implements the transport.Stream interface. 116 | func (s *stream) ClosedChan() <-chan struct{} { 117 | return s.Context().Done() 118 | } 119 | -------------------------------------------------------------------------------- /pkg/transport/quic/transport.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package quic 29 | 30 | import ( 31 | "context" 32 | 33 | "github.com/desertbit/closer/v3" 34 | "github.com/desertbit/orbit/pkg/transport" 35 | quic "github.com/quic-go/quic-go" 36 | ) 37 | 38 | type qTransport struct { 39 | opts *Options 40 | } 41 | 42 | func NewTransport(opts *Options) (t transport.Transport, err error) { 43 | // Set the default options. 44 | opts.setDefaults() 45 | 46 | // Validate the options. 47 | err = opts.validate() 48 | if err != nil { 49 | return 50 | } 51 | 52 | t = &qTransport{opts: opts} 53 | 54 | return 55 | } 56 | 57 | func (q *qTransport) Dial(cl closer.Closer, ctx context.Context, addr string) (transport.Conn, error) { 58 | // Create a quic connection. 59 | qs, err := quic.DialAddr(ctx, addr, q.opts.TLSConfig, q.opts.Config) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | // Create a new session. 65 | return newSession(cl, qs) 66 | } 67 | 68 | func (q *qTransport) Listen(cl closer.Closer, addr string) (transport.Listener, error) { 69 | // Create the quic listener. 70 | ln, err := quic.ListenAddr(addr, q.opts.TLSConfig, q.opts.Config) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | // Create a new listener. 76 | return newListener(cl, ln), nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/transport/transport.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package transport 29 | 30 | import ( 31 | "context" 32 | "net" 33 | 34 | "github.com/desertbit/closer/v3" 35 | ) 36 | 37 | type Transport interface { 38 | Dial(cl closer.Closer, ctx context.Context, addr string) (Conn, error) 39 | Listen(cl closer.Closer, addr string) (Listener, error) 40 | } 41 | 42 | type Conn interface { 43 | closer.Closer 44 | 45 | // AcceptStream returns the next stream opened by the peer, blocking until one is available. 46 | AcceptStream(context.Context) (Stream, error) 47 | 48 | // OpenStream opens a new bidirectional stream. 49 | // There is no signaling to the peer about new streams. 50 | // The peer can only accept the stream after data has been sent on it. 51 | OpenStream(context.Context) (Stream, error) 52 | 53 | // LocalAddr returns the local address. 54 | LocalAddr() net.Addr 55 | 56 | // RemoteAddr returns the address of the peer. 57 | RemoteAddr() net.Addr 58 | 59 | // IsClosedError checks whenever the passed error is a closed connection error. 60 | IsClosedError(error) bool 61 | } 62 | 63 | type Stream interface { 64 | net.Conn 65 | 66 | // IsClosed returns true, if the stream has been closed locally or by the remote peer. 67 | IsClosed() bool 68 | 69 | // ClosedChan returns a closed channel as soon as the stream closes. 70 | ClosedChan() <-chan struct{} 71 | } 72 | 73 | type Listener interface { 74 | closer.Closer 75 | 76 | // Accept waits for and returns the next connection to the listener. 77 | // The listener must close the new connection if the listener is closed. 78 | Accept() (Conn, error) 79 | 80 | // Addr returns the listener's network address. 81 | Addr() net.Addr 82 | } 83 | -------------------------------------------------------------------------------- /pkg/transport/yamux/listener.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package yamux 29 | 30 | import ( 31 | "net" 32 | 33 | "github.com/desertbit/closer/v3" 34 | "github.com/desertbit/orbit/pkg/transport" 35 | "github.com/desertbit/yamux" 36 | ) 37 | 38 | var _ transport.Listener = &listener{} 39 | 40 | type listener struct { 41 | closer.Closer 42 | 43 | ln net.Listener 44 | conf *yamux.Config 45 | } 46 | 47 | func newListener(cl closer.Closer, ln net.Listener, conf *yamux.Config) transport.Listener { 48 | l := &listener{ 49 | Closer: cl, 50 | ln: ln, 51 | conf: conf, 52 | } 53 | l.OnClosing(ln.Close) 54 | return l 55 | } 56 | 57 | // Accept waits for and returns the next connection to the listener. 58 | func (l *listener) Accept() (transport.Conn, error) { 59 | c, err := l.ln.Accept() 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | return newSession(l.CloserOneWay(), c, true, l.conf) 65 | } 66 | 67 | // Addr returns the listener's network address. 68 | func (l *listener) Addr() net.Addr { 69 | return l.ln.Addr() 70 | } 71 | -------------------------------------------------------------------------------- /pkg/transport/yamux/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package yamux 29 | 30 | import ( 31 | "crypto/tls" 32 | "fmt" 33 | "os" 34 | "time" 35 | 36 | "github.com/desertbit/yamux" 37 | "github.com/rs/zerolog" 38 | ) 39 | 40 | var ( 41 | defaultConfig = &yamux.Config{ 42 | AcceptBacklog: 256, 43 | EnableKeepAlive: true, 44 | KeepAliveInterval: 30 * time.Second, 45 | ConnectionWriteTimeout: 10 * time.Second, 46 | MaxStreamWindowSize: 256 * 1024, 47 | LogOutput: defaultLogger(), 48 | } 49 | ) 50 | 51 | type Options struct { 52 | // TODO: 53 | Config *yamux.Config 54 | 55 | // TODO: 56 | // If nil, no encryption. 57 | TLSConfig *tls.Config 58 | 59 | // The network type. Defaults to 'tcp'. 60 | Network string 61 | } 62 | 63 | func (o *Options) setDefaults() { 64 | if o.Config == nil { 65 | o.Config = defaultConfig 66 | } 67 | if o.Network == "" { 68 | o.Network = "tcp" 69 | } 70 | } 71 | 72 | func (o *Options) validate() (err error) { 73 | err = yamux.VerifyConfig(o.Config) 74 | if err != nil { 75 | return 76 | } 77 | if o.Network == "" { 78 | return fmt.Errorf("no network type specified") 79 | } 80 | return 81 | } 82 | 83 | func defaultLogger() *zerolog.Logger { 84 | l := zerolog.New(zerolog.ConsoleWriter{ 85 | Out: os.Stderr, 86 | TimeFormat: time.RFC3339, 87 | }).With().Timestamp().Str("component", "orbit.yamux").Logger() 88 | return &l 89 | } 90 | -------------------------------------------------------------------------------- /pkg/transport/yamux/session.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package yamux 29 | 30 | import ( 31 | "context" 32 | "errors" 33 | "io" 34 | "net" 35 | 36 | "github.com/desertbit/closer/v3" 37 | "github.com/desertbit/orbit/pkg/transport" 38 | "github.com/desertbit/yamux" 39 | ) 40 | 41 | var _ transport.Conn = &session{} 42 | 43 | type session struct { 44 | closer.Closer 45 | 46 | conn net.Conn 47 | ys *yamux.Session 48 | } 49 | 50 | func newSession(cl closer.Closer, conn net.Conn, isServer bool, conf *yamux.Config) (s *session, err error) { 51 | s = &session{ 52 | Closer: cl, 53 | conn: conn, 54 | } 55 | s.OnClosing(conn.Close) 56 | 57 | // Always close on error. 58 | defer func() { 59 | if err != nil { 60 | s.Close_() 61 | } 62 | }() 63 | 64 | // Create a new yamux session. 65 | if isServer { 66 | s.ys, err = yamux.Server(conn, conf) 67 | } else { 68 | s.ys, err = yamux.Client(conn, conf) 69 | } 70 | if err != nil { 71 | return 72 | } 73 | s.OnClosing(s.ys.Close) 74 | 75 | // Always close if the yamux session closes. 76 | go func() { 77 | select { 78 | case <-s.ClosingChan(): 79 | case <-s.ys.ClosedChan(): 80 | } 81 | s.Close_() 82 | }() 83 | 84 | return s, nil 85 | } 86 | 87 | // LocalAddr returns the local address. 88 | func (s *session) LocalAddr() net.Addr { 89 | return s.conn.LocalAddr() 90 | } 91 | 92 | // RemoteAddr returns the address of the peer. 93 | func (s *session) RemoteAddr() net.Addr { 94 | return s.conn.RemoteAddr() 95 | } 96 | 97 | // AcceptStream returns the next stream opened by the peer, blocking until one is available. 98 | func (s *session) AcceptStream(ctx context.Context) (transport.Stream, error) { 99 | stream, err := s.ys.AcceptStreamWithContext(ctx) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | return newStream(stream), nil 105 | } 106 | 107 | // OpenStream opens a new bidirectional stream. 108 | // There is no signaling to the peer about new streams: 109 | // The peer can only accept the stream after data has been sent on the stream. 110 | func (s *session) OpenStream(ctx context.Context) (transport.Stream, error) { 111 | // TODO: Fork the yamux package and implement the context cancel handling. 112 | stream, err := s.ys.OpenStream() 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | return newStream(stream), nil 118 | } 119 | 120 | func (s *session) IsClosedError(err error) bool { 121 | return errors.Is(err, io.EOF) 122 | } 123 | -------------------------------------------------------------------------------- /pkg/transport/yamux/stream.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package yamux 29 | 30 | import ( 31 | "io" 32 | 33 | "github.com/desertbit/orbit/pkg/transport" 34 | "github.com/desertbit/yamux" 35 | ) 36 | 37 | var _ transport.Stream = &stream{} 38 | 39 | type stream struct { 40 | *yamux.Stream 41 | } 42 | 43 | func newStream(s *yamux.Stream) *stream { 44 | return &stream{Stream: s} 45 | } 46 | 47 | // Implements the transport.Stream interface. 48 | func (s *stream) Write(p []byte) (n int, err error) { 49 | // Check for the close error code from a CancelRead peer call. 50 | n, err = s.Stream.Write(p) 51 | if err != nil { 52 | if err == yamux.ErrStreamClosed || err == yamux.ErrConnectionReset { 53 | err = io.EOF 54 | } 55 | return 56 | } 57 | return 58 | } 59 | -------------------------------------------------------------------------------- /pkg/transport/yamux/transport.go: -------------------------------------------------------------------------------- 1 | /* 2 | * ORBIT - Interlink Remote Applications 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2020 Roland Singer 7 | * Copyright (c) 2020 Sebastian Borchers 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | package yamux 29 | 30 | import ( 31 | "context" 32 | "crypto/tls" 33 | "net" 34 | 35 | "github.com/desertbit/closer/v3" 36 | "github.com/desertbit/orbit/pkg/transport" 37 | ) 38 | 39 | type yTransport struct { 40 | opts *Options 41 | } 42 | 43 | func NewTransport(opts *Options) (t transport.Transport, err error) { 44 | // Set the default options. 45 | opts.setDefaults() 46 | 47 | // Validate the options. 48 | err = opts.validate() 49 | if err != nil { 50 | return 51 | } 52 | 53 | t = &yTransport{opts: opts} 54 | 55 | return 56 | } 57 | 58 | func (t *yTransport) Dial(cl closer.Closer, ctx context.Context, addr string) (tc transport.Conn, err error) { 59 | // Open the connection. 60 | var conn net.Conn 61 | if t.opts.TLSConfig != nil { 62 | // TODO: Cancel is deprecated, remove once https://github.com/golang/go/issues/18482 is merged and replace with DialContext. 63 | dl := &net.Dialer{Cancel: ctx.Done()} 64 | conn, err = tls.DialWithDialer(dl, t.opts.Network, addr, t.opts.TLSConfig) 65 | } else { 66 | conn, err = (&net.Dialer{}).DialContext(ctx, t.opts.Network, addr) 67 | } 68 | if err != nil { 69 | return 70 | } 71 | 72 | return newSession(cl, conn, false, t.opts.Config) 73 | } 74 | 75 | func (t *yTransport) Listen(cl closer.Closer, addr string) (tl transport.Listener, err error) { 76 | // Create the listener. 77 | var ln net.Listener 78 | if t.opts.TLSConfig != nil { 79 | ln, err = tls.Listen(t.opts.Network, addr, t.opts.TLSConfig) 80 | } else { 81 | ln, err = net.Listen(t.opts.Network, addr) 82 | } 83 | if err != nil { 84 | return 85 | } 86 | 87 | return newListener(cl, ln, t.opts.Config), nil 88 | } 89 | --------------------------------------------------------------------------------