{{.Error}}
72 | {{end}} 73 | {{if .Username}} 74 |Welcome {{.Username}}!
76 |You can now visit other pages.
77 | {{else}} 78 | 84 | {{end}} 85 | 86 | 87 | `)) 88 | 89 | func UsernameOnlyLoggingForm(w http.ResponseWriter, r *http.Request) error { 90 | return loggingTemplate.Execute(w, nil) 91 | } 92 | 93 | func HandleUsernameOnlyLogging(w http.ResponseWriter, r *http.Request, redirect string) error { 94 | username := r.FormValue("username") 95 | if strings.TrimSpace(username) == "" { 96 | return loggingTemplate.Execute(w, LoggingData{ 97 | Error: "Please enter a valid username", 98 | }) 99 | } 100 | http.SetCookie(w, &http.Cookie{ 101 | Name: unameCookie, 102 | Value: username, 103 | Path: "/", 104 | }) 105 | if redirect != "" { 106 | http.Redirect(w, r, redirect, http.StatusFound) 107 | return nil 108 | } 109 | return loggingTemplate.Execute(w, LoggingData{ 110 | Username: username, 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /pkg/app/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/google/cloud-android-orchestration/pkg/app/accounts" 21 | "github.com/google/cloud-android-orchestration/pkg/app/database" 22 | "github.com/google/cloud-android-orchestration/pkg/app/encryption" 23 | "github.com/google/cloud-android-orchestration/pkg/app/instances" 24 | "github.com/google/cloud-android-orchestration/pkg/app/secrets" 25 | 26 | apiv1 "github.com/google/cloud-android-orchestration/api/v1" 27 | toml "github.com/pelletier/go-toml" 28 | ) 29 | 30 | type Config struct { 31 | WebStaticFilesPath string 32 | CORSAllowedOrigins []string 33 | AccountManager accounts.Config 34 | SecretManager secrets.Config 35 | InstanceManager instances.Config 36 | EncryptionService encryption.Config 37 | DatabaseService database.Config 38 | WebRTC apiv1.InfraConfig 39 | } 40 | 41 | const DefaultConfFile = "conf.toml" 42 | const ConfFileEnvVar = "CONFIG_FILE" 43 | 44 | func LoadConfig() (*Config, error) { 45 | confFile := os.Getenv(ConfFileEnvVar) 46 | if confFile == "" { 47 | confFile = DefaultConfFile 48 | } 49 | file, err := os.Open(confFile) 50 | if err != nil { 51 | return nil, err 52 | } 53 | decoder := toml.NewDecoder(file) 54 | var cfg Config 55 | err = decoder.Decode(&cfg) 56 | return &cfg, err 57 | } 58 | -------------------------------------------------------------------------------- /pkg/app/database/database.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package database 16 | 17 | import ( 18 | "github.com/google/cloud-android-orchestration/pkg/app/session" 19 | ) 20 | 21 | type Service interface { 22 | // Credentials are usually stored encrypted hence the []byte type. 23 | // If no credentials are available for the given user Fetch returns nil, nil. 24 | FetchBuildAPICredentials(username string) ([]byte, error) 25 | // Store new credentials or overwrite existing ones for the given user. 26 | StoreBuildAPICredentials(username string, credentials []byte) error 27 | DeleteBuildAPICredentials(username string) error 28 | // Create or update a user session. 29 | CreateOrUpdateSession(s session.Session) error 30 | // Fetch a session. Returns nil, nil if the session doesn't exist. 31 | FetchSession(key string) (*session.Session, error) 32 | // Delete a session. Won't return error if the session doesn't exist. 33 | DeleteSession(key string) error 34 | } 35 | 36 | type Config struct { 37 | Type string 38 | Spanner *SpannerConfig 39 | } 40 | -------------------------------------------------------------------------------- /pkg/app/database/memorydb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package database 16 | 17 | import ( 18 | "github.com/google/cloud-android-orchestration/pkg/app/session" 19 | ) 20 | 21 | const InMemoryDBType = "InMemory" 22 | 23 | // Simple in memory database to use for testing or local development. 24 | type InMemoryDBService struct { 25 | credentials map[string][]byte 26 | session session.Session 27 | } 28 | 29 | func NewInMemoryDBService() *InMemoryDBService { 30 | return &InMemoryDBService{ 31 | credentials: make(map[string][]byte), 32 | } 33 | } 34 | 35 | func (dbs *InMemoryDBService) FetchBuildAPICredentials(username string) ([]byte, error) { 36 | return dbs.credentials[username], nil 37 | } 38 | 39 | func (dbs *InMemoryDBService) StoreBuildAPICredentials(username string, credentials []byte) error { 40 | dbs.credentials[username] = credentials 41 | return nil 42 | } 43 | 44 | func (dbs *InMemoryDBService) DeleteBuildAPICredentials(username string) error { 45 | delete(dbs.credentials, username) 46 | return nil 47 | } 48 | 49 | func (dbs *InMemoryDBService) CreateOrUpdateSession(s session.Session) error { 50 | dbs.session = s 51 | return nil 52 | } 53 | 54 | func (dbs *InMemoryDBService) FetchSession(key string) (*session.Session, error) { 55 | if dbs.session.Key != key { 56 | return nil, nil 57 | } 58 | sessionCopy := dbs.session 59 | return &sessionCopy, nil 60 | } 61 | 62 | func (dbs *InMemoryDBService) DeleteSession(key string) error { 63 | if dbs.session.Key == key { 64 | dbs.session = session.Session{} 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/app/encryption/encryption.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package encryption 16 | 17 | type Service interface { 18 | Encrypt(plaintext []byte) ([]byte, error) 19 | Decrypt(ciphertext []byte) ([]byte, error) 20 | } 21 | 22 | type Config struct { 23 | Type string 24 | GCPKMS *GCPKMSConfig 25 | } 26 | -------------------------------------------------------------------------------- /pkg/app/encryption/fake.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package encryption 16 | 17 | const FakeESType = "Fake" 18 | 19 | // A simple and very insecure implementation of an encryption service to be used for testing and 20 | // local development. 21 | type FakeEncryptionService struct{} 22 | 23 | func NewFakeEncryptionService() *FakeEncryptionService { 24 | return &FakeEncryptionService{} 25 | } 26 | 27 | func (es *FakeEncryptionService) Encrypt(plaintext []byte) ([]byte, error) { 28 | // Pretend to encrypt/decrypt messages by flipping the bits in the message. That ensures the 29 | // encrypted message is different than the original. 30 | const mask byte = 255 31 | res := make([]byte, len(plaintext)) 32 | for i := 0; i < len(plaintext); i += 1 { 33 | res[i] = plaintext[i] ^ mask 34 | } 35 | return res, nil 36 | } 37 | 38 | func (es *FakeEncryptionService) Decrypt(ciphertext []byte) ([]byte, error) { 39 | // Same procedure to decrypt 40 | return es.Encrypt(ciphertext) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/app/encryption/gcpkms.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package encryption 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | kms "cloud.google.com/go/kms/apiv1" 22 | "cloud.google.com/go/kms/apiv1/kmspb" 23 | ) 24 | 25 | const GCPKMSESType = "GCP_KMS" // GCP's KMS service. 26 | 27 | type GCPKMSConfig struct { 28 | KeyName string 29 | } 30 | 31 | type GCPKMSEncryptionService struct { 32 | keyName string 33 | } 34 | 35 | func NewGCPKMSEncryptionService(keyName string) *GCPKMSEncryptionService { 36 | return &GCPKMSEncryptionService{keyName} 37 | } 38 | 39 | func (s *GCPKMSEncryptionService) Encrypt(plaintext []byte) ([]byte, error) { 40 | ctx := context.TODO() 41 | client, err := kms.NewKeyManagementClient(ctx) 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to instantiate KMS client: %w", err) 44 | } 45 | defer client.Close() 46 | req := &kmspb.EncryptRequest{ 47 | Name: s.keyName, 48 | Plaintext: plaintext, 49 | } 50 | result, err := client.Encrypt(ctx, req) 51 | if err != nil { 52 | return nil, fmt.Errorf("failed encryption request: %w", err) 53 | } 54 | return result.Ciphertext, nil 55 | } 56 | 57 | func (s *GCPKMSEncryptionService) Decrypt(ciphertext []byte) ([]byte, error) { 58 | ctx := context.TODO() 59 | client, err := kms.NewKeyManagementClient(ctx) 60 | if err != nil { 61 | return nil, fmt.Errorf("failed to instantiate KMS client: %w", err) 62 | } 63 | defer client.Close() 64 | req := &kmspb.DecryptRequest{ 65 | Name: s.keyName, 66 | Ciphertext: ciphertext, 67 | } 68 | result, err := client.Decrypt(ctx, req) 69 | if err != nil { 70 | return nil, fmt.Errorf("failed encryption request: %w", err) 71 | } 72 | return result.Plaintext, nil 73 | } 74 | -------------------------------------------------------------------------------- /pkg/app/errors/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package errors 16 | 17 | import ( 18 | "net/http" 19 | 20 | apiv1 "github.com/google/cloud-android-orchestration/api/v1" 21 | ) 22 | 23 | type AppError struct { 24 | Msg string 25 | StatusCode int 26 | Err error 27 | } 28 | 29 | func (e *AppError) Error() string { 30 | if e.Err != nil { 31 | return e.Msg + ": " + e.Err.Error() 32 | } 33 | return e.Msg 34 | } 35 | func (e *AppError) Unwrap() error { 36 | return e.Err 37 | } 38 | 39 | func (e *AppError) JSONResponse() apiv1.Error { 40 | // Include only the high level error message in the error response, the 41 | // lower level errors are just for logging 42 | return apiv1.Error{ 43 | Code: e.StatusCode, 44 | ErrorMsg: e.Msg, 45 | } 46 | } 47 | 48 | func NewNotFoundError(msg string, e error) error { 49 | return &AppError{Msg: msg, StatusCode: http.StatusNotFound, Err: e} 50 | } 51 | 52 | func NewBadRequestError(msg string, e error) error { 53 | return &AppError{Msg: msg, StatusCode: http.StatusBadRequest, Err: e} 54 | } 55 | 56 | func NewMethodNotAllowedError(msg string, e error) error { 57 | return &AppError{Msg: msg, StatusCode: http.StatusMethodNotAllowed, Err: e} 58 | } 59 | 60 | func NewInternalError(msg string, e error) error { 61 | return &AppError{Msg: msg, StatusCode: http.StatusInternalServerError, Err: e} 62 | } 63 | 64 | func NewUnauthenticatedError(msg string, e error) error { 65 | // 401 Unauthorized semantically means "Unauthenticated, while authorization errors are to be 66 | // returned with 403 Forbidden according to 67 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses. 68 | return &AppError{Msg: msg, StatusCode: http.StatusUnauthorized, Err: e} 69 | } 70 | 71 | func NewForbiddenError(msg string, e error) error { 72 | return &AppError{Msg: msg, StatusCode: http.StatusForbidden, Err: e} 73 | } 74 | 75 | func NewServiceUnavailableError(msg string, e error) error { 76 | return &AppError{Msg: msg, StatusCode: http.StatusServiceUnavailable, Err: e} 77 | } 78 | -------------------------------------------------------------------------------- /pkg/app/instances/docker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package instances 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/google/go-cmp/cmp" 21 | ) 22 | 23 | func TestEncodeOperationNameSucceeds(t *testing.T) { 24 | if diff := cmp.Diff("foo_bar", EncodeOperationName("foo", "bar")); diff != "" { 25 | t.Errorf("encoded operation name mismatch (-want +got):\n%s", diff) 26 | } 27 | } 28 | 29 | func TestDecodeOperationSucceeds(t *testing.T) { 30 | gotOpType, gotHost, err := DecodeOperationName("foo_bar") 31 | if err != nil { 32 | t.Errorf("got error while decoding operation name: %+v", err) 33 | } 34 | if diff := cmp.Diff(OPType("foo"), gotOpType); diff != "" { 35 | t.Errorf("decoded operation type mismatch (-want +got):\n%s", diff) 36 | } 37 | if diff := cmp.Diff("bar", gotHost); diff != "" { 38 | t.Errorf("decoded host mismatch (-want +got):\n%s", diff) 39 | } 40 | } 41 | 42 | func TestDecodeOperationFailsEmptyString(t *testing.T) { 43 | _, _, err := DecodeOperationName("") 44 | if err == nil { 45 | t.Errorf("expected error") 46 | } 47 | } 48 | 49 | func TestDecodeOperationFailsMissingUnderscore(t *testing.T) { 50 | _, _, err := DecodeOperationName("foobar") 51 | if err == nil { 52 | t.Errorf("expected error") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/app/instances/hostclient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package instances 16 | 17 | import ( 18 | "bytes" 19 | "crypto/tls" 20 | "encoding/json" 21 | "fmt" 22 | "log" 23 | "net/http" 24 | "net/http/httputil" 25 | "net/url" 26 | 27 | apiv1 "github.com/google/cloud-android-orchestration/api/v1" 28 | ) 29 | 30 | type NetHostClient struct { 31 | url *url.URL 32 | client *http.Client 33 | } 34 | 35 | func NewNetHostClient(url *url.URL, allowSelfSigned bool) *NetHostClient { 36 | ret := &NetHostClient{ 37 | url: url, 38 | client: http.DefaultClient, 39 | } 40 | if allowSelfSigned { 41 | // This creates a transport similar to http.DefaultTransport according to 42 | // https://pkg.go.dev/net/http#RoundTripper. The object needs to be created 43 | // instead of copied from http.DefaultTransport because it has a mutex which 44 | // could be copied in locked state and produce a copy that's unusable because 45 | // nothing will ever unlock it. 46 | defaultTransport := http.DefaultTransport.(*http.Transport) 47 | transport := &http.Transport{ 48 | Proxy: defaultTransport.Proxy, 49 | // Reusing the same dial context allows reusing connections across transport objects. 50 | DialContext: defaultTransport.DialContext, 51 | ForceAttemptHTTP2: defaultTransport.ForceAttemptHTTP2, 52 | MaxIdleConns: defaultTransport.MaxIdleConns, 53 | IdleConnTimeout: defaultTransport.IdleConnTimeout, 54 | TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout, 55 | ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout, 56 | } 57 | transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 58 | ret.client = &http.Client{Transport: transport} 59 | } 60 | return ret 61 | } 62 | 63 | func (c *NetHostClient) Get(path, query string, out *HostResponse) (int, error) { 64 | url := *c.url // Shallow copy 65 | url.Path = path 66 | url.RawQuery = query 67 | res, err := c.client.Get(url.String()) 68 | if err != nil { 69 | return -1, fmt.Errorf("failed to connect to device host: %w", err) 70 | } 71 | defer res.Body.Close() 72 | if out != nil { 73 | err = parseReply(res, out.Result, out.Error) 74 | } 75 | return res.StatusCode, err 76 | } 77 | 78 | func (c *NetHostClient) Post(path, query string, bodyJSON any, out *HostResponse) (int, error) { 79 | bodyStr, err := json.Marshal(bodyJSON) 80 | if err != nil { 81 | return -1, fmt.Errorf("failed to parse JSON request: %w", err) 82 | } 83 | url := *c.url // Shallow copy 84 | url.Path = path 85 | url.RawQuery = query 86 | res, err := c.client.Post(url.String(), "application/json", bytes.NewBuffer(bodyStr)) 87 | if err != nil { 88 | return -1, fmt.Errorf("failed to connecto to device host: %w", err) 89 | } 90 | defer res.Body.Close() 91 | if out != nil { 92 | err = parseReply(res, out.Result, out.Error) 93 | } 94 | return res.StatusCode, err 95 | } 96 | 97 | func (c *NetHostClient) GetReverseProxy() *httputil.ReverseProxy { 98 | devProxy := httputil.NewSingleHostReverseProxy(c.url) 99 | if c.client != http.DefaultClient { 100 | // Make sure the reverse proxy has the same customizations as the http client. 101 | devProxy.Transport = c.client.Transport 102 | } 103 | devProxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { 104 | log.Printf("request %q failed: proxy error: %v", r.Method+" "+r.URL.Path, err) 105 | w.Header().Add("x-cutf-proxy", "co-host") 106 | w.WriteHeader(http.StatusBadGateway) 107 | } 108 | return devProxy 109 | } 110 | 111 | func parseReply(res *http.Response, resObj any, resErr *apiv1.Error) error { 112 | var err error 113 | dec := json.NewDecoder(res.Body) 114 | if res.StatusCode < 200 || res.StatusCode > 299 { 115 | err = dec.Decode(resErr) 116 | } else { 117 | err = dec.Decode(resObj) 118 | } 119 | if err != nil { 120 | return fmt.Errorf("failed to parse device response: %w", err) 121 | } 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /pkg/app/instances/instances.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package instances 16 | 17 | import ( 18 | "net/http/httputil" 19 | 20 | apiv1 "github.com/google/cloud-android-orchestration/api/v1" 21 | "github.com/google/cloud-android-orchestration/pkg/app/accounts" 22 | ) 23 | 24 | type Manager interface { 25 | // List zones 26 | ListZones() (*apiv1.ListZonesResponse, error) 27 | // Creates a host instance. 28 | CreateHost(zone string, req *apiv1.CreateHostRequest, user accounts.User) (*apiv1.Operation, error) 29 | // List hosts 30 | ListHosts(zone string, user accounts.User, req *ListHostsRequest) (*apiv1.ListHostsResponse, error) 31 | // Deletes the given host instance. 32 | DeleteHost(zone string, user accounts.User, name string) (*apiv1.Operation, error) 33 | // Waits until operation is DONE or earlier. If DONE return the expected response of the operation. If the 34 | // original method returns no data on success, such as `Delete`, response will be empty. If the original method 35 | // is standard `Get`/`Create`/`Update`, the response should be the relevant resource. 36 | WaitOperation(zone string, user accounts.User, name string) (any, error) 37 | // Creates a connector to the given host. 38 | GetHostClient(zone string, host string) (HostClient, error) 39 | } 40 | 41 | type HostClient interface { 42 | // Get and Post requests return the HTTP status code or an error. 43 | // The response body is parsed into the res output parameter if provided. 44 | Get(URLPath, URLQuery string, res *HostResponse) (int, error) 45 | Post(URLPath, URLQuery string, bodyJSON any, res *HostResponse) (int, error) 46 | GetReverseProxy() *httputil.ReverseProxy 47 | } 48 | 49 | type HostResponse struct { 50 | Result any 51 | Error *apiv1.Error 52 | } 53 | 54 | type ListHostsRequest struct { 55 | // The maximum number of results per page that should be returned. If the number of available results is larger 56 | // than MaxResults, a `NextPageToken` will be returned which can be used to get the next page of results 57 | // in subsequent List requests. 58 | MaxResults uint32 59 | // Specifies a page token to use. 60 | // Use the `NextPageToken` value returned by a previous List request. 61 | PageToken string 62 | } 63 | 64 | type IMType string 65 | 66 | type Config struct { 67 | Type IMType 68 | // The protocol the host orchestrator expects, either http or https 69 | HostOrchestratorProtocol string 70 | AllowSelfSignedHostSSLCertificate bool 71 | GCP *GCPIMConfig 72 | UNIX *UNIXIMConfig 73 | Docker *DockerIMConfig 74 | } 75 | -------------------------------------------------------------------------------- /pkg/app/instances/local.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package instances 16 | 17 | import ( 18 | "fmt" 19 | "net/url" 20 | 21 | apiv1 "github.com/google/cloud-android-orchestration/api/v1" 22 | "github.com/google/cloud-android-orchestration/pkg/app/accounts" 23 | ) 24 | 25 | const UnixIMType IMType = "unix" 26 | 27 | type UNIXIMConfig struct { 28 | HostOrchestratorPort int 29 | } 30 | 31 | // Implements the Manager interface providing access to the first 32 | // device in the local host orchestrator. 33 | // This implementation is useful for both development and testing 34 | type LocalInstanceManager struct { 35 | config Config 36 | } 37 | 38 | func NewLocalInstanceManager(cfg Config) *LocalInstanceManager { 39 | return &LocalInstanceManager{ 40 | config: cfg, 41 | } 42 | } 43 | 44 | func (m *LocalInstanceManager) GetHostAddr(_ string, _ string) (string, error) { 45 | return "127.0.0.1", nil 46 | } 47 | 48 | func (m *LocalInstanceManager) GetHostURL(zone string, host string) (*url.URL, error) { 49 | addr, err := m.GetHostAddr(zone, host) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return url.Parse(fmt.Sprintf("%s://%s:%d", m.config.HostOrchestratorProtocol, addr, m.config.UNIX.HostOrchestratorPort)) 54 | } 55 | 56 | func (m *LocalInstanceManager) ListZones() (*apiv1.ListZonesResponse, error) { 57 | return &apiv1.ListZonesResponse{ 58 | Items: []*apiv1.Zone{{ 59 | Name: "local", 60 | }}, 61 | }, nil 62 | } 63 | 64 | func (m *LocalInstanceManager) CreateHost(_ string, _ *apiv1.CreateHostRequest, _ accounts.User) (*apiv1.Operation, error) { 65 | return &apiv1.Operation{ 66 | Name: "Create Host", 67 | Done: true, 68 | }, nil 69 | } 70 | 71 | func (m *LocalInstanceManager) ListHosts(zone string, user accounts.User, req *ListHostsRequest) (*apiv1.ListHostsResponse, error) { 72 | return &apiv1.ListHostsResponse{ 73 | Items: []*apiv1.HostInstance{{ 74 | Name: "local", 75 | }}, 76 | }, nil 77 | } 78 | 79 | func (m *LocalInstanceManager) DeleteHost(zone string, user accounts.User, name string) (*apiv1.Operation, error) { 80 | return nil, fmt.Errorf("%T#DeleteHost is not implemented", *m) 81 | } 82 | 83 | func (m *LocalInstanceManager) WaitOperation(zone string, user accounts.User, name string) (any, error) { 84 | return nil, fmt.Errorf("%T#WaitOperation is not implemented", *m) 85 | } 86 | 87 | func (m *LocalInstanceManager) GetHostClient(zone string, host string) (HostClient, error) { 88 | url, err := m.GetHostURL(zone, host) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return NewNetHostClient(url, m.config.AllowSelfSignedHostSSLCertificate), nil 93 | } 94 | -------------------------------------------------------------------------------- /pkg/app/oauth2/oauth2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package oauth2 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "net/http" 21 | "net/url" 22 | 23 | "github.com/google/cloud-android-orchestration/pkg/app/secrets" 24 | 25 | "golang.org/x/oauth2" 26 | "golang.org/x/oauth2/google" 27 | ) 28 | 29 | type OAuth2Config struct { 30 | Provider string 31 | RedirectURL string 32 | } 33 | 34 | const ( 35 | GoogleOAuth2Provider = "Google" 36 | ) 37 | 38 | type Helper struct { 39 | oauth2.Config 40 | Revoke func(*oauth2.Token) error 41 | } 42 | 43 | // Build a oauth2.Config object with Google as the provider. 44 | func NewGoogleOAuth2Helper(redirectURL string, sm secrets.SecretManager) *Helper { 45 | return &Helper{ 46 | Config: oauth2.Config{ 47 | ClientID: sm.OAuth2ClientID(), 48 | ClientSecret: sm.OAuth2ClientSecret(), 49 | Scopes: []string{ 50 | "https://www.googleapis.com/auth/androidbuild.internal", 51 | "openid", 52 | "email", 53 | }, 54 | RedirectURL: redirectURL, 55 | Endpoint: google.Endpoint, 56 | }, 57 | Revoke: RevokeGoogleOAuth2Token, 58 | } 59 | } 60 | 61 | func RevokeGoogleOAuth2Token(tk *oauth2.Token) error { 62 | if tk == nil { 63 | return fmt.Errorf("nil Token") 64 | } 65 | _, err := http.DefaultClient.PostForm( 66 | "https://oauth2.googleapis.com/revoke", 67 | url.Values{"token": []string{tk.AccessToken}}) 68 | return err 69 | } 70 | 71 | // ID tokens (from OpenID connect) are presented in JWT format, with the relevant fields in the Claims section. 72 | type IDTokenClaims map[string]interface{} 73 | 74 | func (c IDTokenClaims) Email() (string, error) { 75 | v, ok := c["email"] 76 | if !ok { 77 | return "", errors.New("no email in id token") 78 | } 79 | vstr, ok := v.(string) 80 | if !ok { 81 | return "", errors.New("malformed email in id token") 82 | } 83 | return vstr, nil 84 | } 85 | -------------------------------------------------------------------------------- /pkg/app/secrets/empty.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package secrets 16 | 17 | const EmptySMType = "" 18 | 19 | // A secret manager that always returns empty string. 20 | type EmptySecretManager struct{} 21 | 22 | func NewEmptySecretManager() *EmptySecretManager { 23 | return &EmptySecretManager{} 24 | } 25 | 26 | func (sm *EmptySecretManager) OAuth2ClientID() string { 27 | return "" 28 | } 29 | 30 | func (sm *EmptySecretManager) OAuth2ClientSecret() string { 31 | return "" 32 | } 33 | -------------------------------------------------------------------------------- /pkg/app/secrets/gcp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package secrets 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | 22 | secretmanager "cloud.google.com/go/secretmanager/apiv1" 23 | "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" 24 | ) 25 | 26 | const GCPSMType = "GCP" 27 | 28 | type GCPSMConfig struct { 29 | OAuth2ClientResourceID string 30 | } 31 | 32 | type ClientSecrets struct { 33 | ClientID string `json:"client_id"` 34 | ClientSecret string `json:"client_secret"` 35 | } 36 | 37 | type GCPSecretManager struct { 38 | secrets ClientSecrets 39 | } 40 | 41 | func NewGCPSecretManager(config *GCPSMConfig) (*GCPSecretManager, error) { 42 | ctx := context.TODO() 43 | client, err := secretmanager.NewClient(ctx) 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to create secret manager client: %w", err) 46 | } 47 | defer client.Close() 48 | 49 | accessRequest := &secretmanagerpb.AccessSecretVersionRequest{Name: config.OAuth2ClientResourceID} 50 | result, err := client.AccessSecretVersion(ctx, accessRequest) 51 | if err != nil { 52 | return nil, fmt.Errorf("failed to access secret: %w", err) 53 | } 54 | sm := &GCPSecretManager{} 55 | err = json.Unmarshal(result.Payload.Data, &sm.secrets) 56 | if err != nil { 57 | return nil, fmt.Errorf("failed to decode secrets: %w", err) 58 | } 59 | return sm, nil 60 | } 61 | 62 | func (s *GCPSecretManager) OAuth2ClientID() string { 63 | return s.secrets.ClientID 64 | } 65 | 66 | func (s *GCPSecretManager) OAuth2ClientSecret() string { 67 | return s.secrets.ClientSecret 68 | } 69 | -------------------------------------------------------------------------------- /pkg/app/secrets/local.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package secrets 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "os" 21 | ) 22 | 23 | const UnixSMType = "unix" 24 | 25 | type UnixSMConfig struct { 26 | SecretFilePath string 27 | } 28 | 29 | // A secret manager that reads secrets from a file in JSON format. 30 | type FromFileSecretManager struct { 31 | ClientID string `json:"client_id"` 32 | ClientSecret string `json:"client_secret"` 33 | } 34 | 35 | func NewFromFileSecretManager(path string) (*FromFileSecretManager, error) { 36 | r, err := os.Open(path) 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to open secrets file: %w", err) 39 | } 40 | dec := json.NewDecoder(r) 41 | var sm FromFileSecretManager 42 | if err := dec.Decode(&sm); err != nil { 43 | return nil, err 44 | } 45 | return &sm, nil 46 | } 47 | 48 | func (sm *FromFileSecretManager) OAuth2ClientID() string { 49 | return sm.ClientID 50 | } 51 | 52 | func (sm *FromFileSecretManager) OAuth2ClientSecret() string { 53 | return sm.ClientSecret 54 | } 55 | -------------------------------------------------------------------------------- /pkg/app/secrets/secrets.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package secrets 16 | 17 | type SecretManager interface { 18 | OAuth2ClientID() string 19 | OAuth2ClientSecret() string 20 | } 21 | 22 | type SMType string 23 | 24 | type Config struct { 25 | Type SMType 26 | GCP *GCPSMConfig 27 | UNIX *UnixSMConfig 28 | } 29 | -------------------------------------------------------------------------------- /pkg/app/session/session.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package session 16 | 17 | type Session struct { 18 | Key string 19 | OAuth2State string 20 | } 21 | -------------------------------------------------------------------------------- /pkg/cli/adbserver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cli 16 | 17 | import ( 18 | "fmt" 19 | "net" 20 | ) 21 | 22 | type ADBServerProxy interface { 23 | Connect(port int) error 24 | Disconnect(port int) error 25 | } 26 | 27 | const ADBServerPort = 5037 28 | 29 | type ADBServerProxyImpl struct{} 30 | 31 | func (p *ADBServerProxyImpl) connect(adbSerial string) error { 32 | msg := fmt.Sprintf("host:connect:%s", adbSerial) 33 | return p.sendMsg(msg) 34 | } 35 | 36 | func (p *ADBServerProxyImpl) Connect(port int) error { 37 | return p.connect(fmt.Sprintf("127.0.0.1:%d", port)) 38 | } 39 | 40 | func (p *ADBServerProxyImpl) disconnect(adbSerial string) error { 41 | msg := fmt.Sprintf("host:disconnect:%s", adbSerial) 42 | return p.sendMsg(msg) 43 | } 44 | 45 | func (p *ADBServerProxyImpl) Disconnect(port int) error { 46 | return p.disconnect(fmt.Sprintf("127.0.0.1:%d", port)) 47 | } 48 | 49 | func (*ADBServerProxyImpl) sendMsg(msg string) error { 50 | msg = fmt.Sprintf("%.4x%s", len(msg), msg) 51 | conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", ADBServerPort)) 52 | if err != nil { 53 | return fmt.Errorf("unable to contact ADB server: %w", err) 54 | } 55 | defer conn.Close() 56 | written := 0 57 | for written < len(msg) { 58 | n, err := conn.Write([]byte(msg[written:])) 59 | if err != nil { 60 | return fmt.Errorf("error sending message to ADB server: %w", err) 61 | } 62 | written += n 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/cli/authz/oauth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package authz 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "log" 22 | "math/rand" 23 | "net/http" 24 | "net/url" 25 | 26 | "golang.org/x/oauth2" 27 | "golang.org/x/oauth2/google" 28 | ) 29 | 30 | func createServer(ch chan string, state string, port string) *http.Server { 31 | return &http.Server{ 32 | Addr: ":" + port, 33 | Handler: http.HandlerFunc(handler(ch, state)), 34 | } 35 | } 36 | 37 | func handler(ch chan string, randState string) func(http.ResponseWriter, *http.Request) { 38 | return func(rw http.ResponseWriter, req *http.Request) { 39 | if req.URL.Path == "/favicon.ico" { 40 | http.Error(rw, "error: visiting /favicon.ico", 404) 41 | return 42 | } 43 | if req.FormValue("state") != randState { 44 | log.Printf("state: %s doesn't match. (expected: %s)", req.FormValue("state"), randState) 45 | http.Error(rw, "invalid state", 500) 46 | return 47 | } 48 | if code := req.FormValue("code"); code != "" { 49 | fmt.Fprintf(rw, "